From 89feff7502659d29f7259f844626260f0df4a15a Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Mon, 23 Mar 2026 10:58:08 -0600 Subject: [PATCH 01/20] Commit --- package-lock.json | 26 ++++---------------------- package.json | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dc9cea6..d772b1d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@electron/notarize": "^2.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@fingerprintjs/fingerprintjs": "^5.0.1", + "@fingerprintjs/fingerprintjs": "^5.1.0", "@fontsource/roboto": "^5.2.6", "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-regular-svg-icons": "^7.0.1", @@ -1723,9 +1723,9 @@ } }, "node_modules/@fingerprintjs/fingerprintjs": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.0.1.tgz", - "integrity": "sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.1.0.tgz", + "integrity": "sha512-8h/CscV3xQ4KSLyXbSK8YFpZ5AaezzHfkl82mn8NJIEWNi1zLfbZSIu7MGGtx4pqa10oejhEk4u0MNutuE63Fw==", "license": "MIT" }, "node_modules/@floating-ui/core": { @@ -15882,24 +15882,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 5b7cf9a8..3ab54f38 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@electron/notarize": "^2.3.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@fingerprintjs/fingerprintjs": "^5.0.1", + "@fingerprintjs/fingerprintjs": "^5.1.0", "@fontsource/roboto": "^5.2.6", "@fortawesome/fontawesome-svg-core": "^7.0.1", "@fortawesome/free-regular-svg-icons": "^7.0.1", From edd2d5cc5332b6d2ab8bffac3e5a708fa368b1e4 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Mon, 23 Mar 2026 13:25:30 -0700 Subject: [PATCH 02/20] setup for mobile copy --- .../PassageDetailsArtifactsMobile.tsx | 1053 +++++++++++++++++ src/renderer/src/routes/PassageDetail.tsx | 28 +- 2 files changed, 1080 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx new file mode 100644 index 00000000..bf8054f7 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -0,0 +1,1053 @@ +import { useState, useContext, useMemo, useRef, useEffect } from 'react'; +import { useGetGlobal, useGlobal } from '../../../context/useGlobal'; +import { + IPassageDetailArtifactsStrings, + Passage, + Section, + MediaFileD, + SectionResourceD, + MediaFile, + ArtifactType, + Resource, + SheetLevel, + ISharedStrings, +} from '../../../model'; +import { arrayMoveImmutable as arrayMove } from 'array-move'; +import { PlayInPlayer } from '../../../context/PlayInPlayer'; +import { useSnackBar } from '../../../hoc/SnackBar'; +import Uploader from '../../Uploader'; +import AddResource from './AddResource'; +import SortableHeader from './SortableHeader'; +import { IRow } from '../../../context/PassageDetailContext'; +import { AltButton } from '../../../control'; +import { AIGenerated, SortableItem } from '.'; +import { + remoteIdGuid, + useSecResCreate, + useMediaResCreate, + useSecResUpdate, + useSecResDelete, + related, + useSecResUserCreate, + useSecResUserRead, + useSecResUserDelete, + useOrganizedBy, + findRecord, + useArtifactCategory, + IArtifactCategory, + ArtifactCategoryType, + mediaFileName, + usePlanType, + usePlan, +} from '../../../crud'; +import BigDialog from '../../../hoc/BigDialog'; +import { BigDialogBp } from '../../../hoc/BigDialogBp'; +import MediaDisplay from '../../MediaDisplay'; +import SelectSharedResource from './SelectSharedResource'; +import SelectProjectResource from './SelectProjectResource'; +import SelectSections from './SelectSections'; +import ResourceData from './ResourceData'; +import { MarkDownType, UriLinkType } from '../../MediaUpload'; +import LimitedMediaPlayer from '../../LimitedMediaPlayer'; +import { + Badge, + Box, + BoxProps, + Grid, + Stack, + styled, + Typography, +} from '@mui/material'; +import { ReplaceRelatedRecord } from '../../../model/baseModel'; +import { PassageResourceButton } from './PassageResourceButton'; +import ProjectResourceConfigure from './ProjectResourceConfigure'; +import { useProjectResourceSave } from './useProjectResourceSave'; +import { UnsavedContext } from '../../../context/UnsavedContext'; +import Confirm from '../../AlertDialog'; +import { + getSegments, + NamedRegions, + removeExtension, + isVisual, + isUrl, +} from '../../../utils'; +import { useOrbitData } from '../../../hoc/useOrbitData'; +import { + RecordIdentity, + RecordKeyMap, + RecordTransformBuilder, +} from '@orbit/records'; +import { shallowEqual, useSelector } from 'react-redux'; +import { + passageDetailArtifactsSelector, + sharedSelector, +} from '../../../selector'; +import { passageTypeFromRef } from '../../../control/passageTypeFromRef'; +import { PassageTypeEnum } from '../../../model/passageType'; +import { VertListDnd } from '../../../hoc/VertListDnd'; +import usePassageDetailContext from '../../../context/usePassageDetailContext'; +import { LaunchLink } from '../../../control/LaunchLink'; +import FindTabs from './FindTabs'; +import { storedCompareKey } from '../../../utils/storedCompareKey'; +import { mediaContentType } from '../../../utils/contentType'; +import { useStepPermissions } from '../../../utils/useStepPermission'; +import FindBibleBrain from './FindBibleBrain'; +import { useHandleLink } from './addLinkKind'; +import { usePassageRef } from './usePassageRef'; +import { MarkDownView } from '../../../control/MarkDownView'; +import { UploadType } from '../../UploadType'; +import { ResourceTypeEnum } from './ResourceTypeEnum'; + +const MediaContainer = styled(Box)(({ theme }) => ({ + marginRight: theme.spacing(2), + marginTop: theme.spacing(1), + width: '100%', + '& audio': { + height: '40px', + display: 'flex', + width: 'inherit', + }, +})); + +export function PassageDetailArtifactsMobile() { + const sectionResources = useOrbitData('sectionresource'); + const mediafiles = useOrbitData('mediafile'); + const artifactTypes = useOrbitData('artifacttype'); + const [memory] = useGlobal('memory'); + const [busy, setBusy] = useGlobal('importexportBusy'); //verified this is not used in a function 2/18/25 + const [remoteBusy] = useGlobal('remoteBusy'); //verified this is not used in a function 2/18/25 + const [offline] = useGlobal('offline'); //verified this is not used in a function 2/18/25 + const [offlineOnly] = useGlobal('offlineOnly'); //will be constant here + const [, setComplete] = useGlobal('progress'); + const { + rowData, + section, + passage, + setSelected, + playItem, + setPlayItem, + setMediaSelected, + itemPlaying, + setItemPlaying, + currentstep, + toggleDone, + forceRefresh, + handleItemPlayEnd, + handleItemTogglePlay, + getProjectResources, + } = usePassageDetailContext(); + const { getOrganizedBy } = useOrganizedBy(); + const { AddSectionResource } = useSecResCreate(section); + const AddSectionResourceUser = useSecResUserCreate(); + const ReadSectionResourceUser = useSecResUserRead(); + const RemoveSectionResourceUser = useSecResUserDelete(); + const AddMediaFileResource = useMediaResCreate(passage, currentstep); + const UpdateSectionResource = useSecResUpdate(); + const DeleteSectionResource = useSecResDelete(); + const { getArtifactCategorys } = useArtifactCategory(); + const catRef = useRef([]); + const [uploadVisible, setUploadVisible] = useState(false); + const [aiGenerated, setAIGenerated] = useState(false); + const [findOpen, setFindOpen] = useState(false); + const [visual, setVisual] = useState(false); + const [sortKey, setSortKey] = useState(0); + const cancelled = useRef(false); + const [displayId, setDisplayId] = useState(''); + const [link, setLink] = useState(); + const [markDown, setMarkDoan] = useState(''); + const [audioScriptureVisible, setAudioScriptureVisible] = useState(false); + const [nonAudio, setNonAudio] = useState(false); + const [sharedResourceVisible, setSharedResourceVisible] = useState(false); + const [projectResourceVisible, setProjectResourceVisible] = useState(false); + const [projResPassageVisible, setProjResPassageVisible] = useState(false); + const [projResWizVisible, setProjResWizVisible] = useState(false); + const [projResSetup, setProjResSetup] = useState(new Array()); + const [editResource, setEditResource] = useState< + SectionResourceD | undefined + >(); + const [allowEditSave, setAllowEditSave] = useState(false); + const [artifactState] = useState<{ id?: string | null }>({}); + // const [artifactTypeId, setArtifactTypeId] = useState(); + const [uploadType, setUploadType] = useState(UploadType.Resource); + const [initDescription, setInitDescription] = useState(''); + const [recordAudio, setRecordAudio] = useState(false); + const mediaRef = useRef(undefined); + const textRef = useRef(undefined); + const catIdRef = useRef(undefined); + const descriptionRef = useRef(''); + + const resourceTypeRef = useRef( + ResourceTypeEnum.sectionResource + ); + const projIdentRef = useRef([]); + const projMediaRef = useRef(undefined); + const [allResources, setAllResources] = useState(false); + const { showMessage } = useSnackBar(); + const [confirm, setConfirm] = useState(''); + const { waitForSave } = useContext(UnsavedContext).state; + const [mediaStart, setMediaStart] = useState(); + const [mediaEnd, setMediaEnd] = useState(); + const [performedBy, setPerformedBy] = useState(''); + const [markdownValue, setMarkdownValue] = useState(''); + const projectResourceSave = useProjectResourceSave(); + const { removeKey } = storedCompareKey(passage, section); + const [plan] = useGlobal('plan'); //will be constant here + const planType = usePlanType(); + const { getPlan } = usePlan(); + const t: IPassageDetailArtifactsStrings = useSelector( + passageDetailArtifactsSelector, + shallowEqual + ); + const ts: ISharedStrings = useSelector(sharedSelector, shallowEqual); + const { canDoSectionStep } = useStepPermissions(); + const hasPermission = canDoSectionStep(currentstep, section); + const [biblebrainClose, setBiblebrainClose] = useState(false); + const getGlobal = useGetGlobal(); + const handleLink = useHandleLink({ passage, setLink }); + const { passageRef } = usePassageRef(); + + const planRec = plan ? getPlan(plan) : null; + const filename = planRec?.attributes?.slug + ? `${planRec.attributes.slug}resource` + : 'resource'; + + const resourceType = useMemo(() => { + const resourceType = artifactTypes.find( + (t) => + t.attributes?.typename === 'resource' && + Boolean(t?.keys?.remoteId) === !offlineOnly + ); + // setArtifactTypeId(resourceType?.id); + artifactState.id = resourceType?.id || null; + return resourceType?.id; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [artifactTypes, offlineOnly]); + + const otherResourcesAvailable = useMemo( + () => rowData.some((r) => r.passageId && r.passageId !== passage.id), + [passage, rowData] + ); + + const handleNonAudio = (value: boolean) => setNonAudio(value); + + const isPassageResource = () => + resourceTypeRef.current === ResourceTypeEnum.passageResource; + const isProjectResource = () => + resourceTypeRef.current === ResourceTypeEnum.projectResource; + + const projResourceType = useMemo(() => { + const resourceType = artifactTypes.find( + (t) => + t.attributes?.typename === 'projectresource' && + Boolean(t?.keys?.remoteId) === !offlineOnly + ); + return resourceType?.id; + }, [artifactTypes, offlineOnly]); + + const handlePlay = (id: string) => { + if (id === playItem) { + setItemPlaying(!itemPlaying); + } else { + const row = rowData.find((r) => r.id === id); + if (row) { + const segs = getSegments( + NamedRegions.ProjectResource, + row.mediafile.attributes.segments + ); + const regions = JSON.parse(segs); + if (regions.length > 0) { + const { start, end } = regions[0]; + setMediaStart(start); + setMediaEnd(end); + setMediaSelected(id, start, end); + return; + } else { + setMediaStart(undefined); + setMediaEnd(undefined); + } + } + setSelected(id, PlayInPlayer.no); + } + }; + + const handleDisplayId = (id: string) => { + setDisplayId(id); + }; + + const handleFinish = () => { + setDisplayId(''); + }; + + const handleLinkId = (id: string) => { + setLink( + rowData.find((r) => r.id === id)?.mediafile?.attributes?.originalFile + ); + }; + + const handleMarkDownId = (id: string) => { + setMarkDoan( + rowData.find((r) => r.id === id)?.mediafile?.attributes?.originalFile ?? + '' + ); + }; + + const handleDone = async (id: string, res: SectionResourceD | null) => { + if (!res) return; + const rec = await ReadSectionResourceUser(res); + if (rec !== null) { + await RemoveSectionResourceUser(res, rec); + } else { + await AddSectionResourceUser(res); + } + toggleDone(id); + setTimeout(() => { + setBusy(true); + forceRefresh(); + setTimeout(() => setBusy(false), 500); + }, 500); + }; + + const handleDelete = (id: string) => setConfirm(id); + const handleDeleteRefused = () => setConfirm(''); + const handleDeleteConfirmed = () => { + setBusy(true); + const secRes = sectionResources.find( + (r) => related(r, 'mediafile') === confirm + ); + if (secRes) { + removeKey(related(secRes, 'mediafile')); + DeleteSectionResource(secRes); + } + setConfirm(''); + setBusy(false); + }; + const handleUploadVisible = (v: boolean) => { + setUploadVisible(v); + }; + + const handleFindVisible = (v: boolean) => { + setFindOpen(v); + }; + + const handleSharedResourceVisible = (v: boolean) => { + setSharedResourceVisible(v); + }; + + const handleProjectResourceVisible = (v: boolean) => { + const complete = getGlobal('progress'); + if (complete === 0 || complete === 100) { + setProjectResourceVisible(v); + } + }; + + const handleProjResPassageVisible = (v: boolean) => { + setProjResPassageVisible(v); + }; + + const handleProjResWizVisible = (v: boolean) => { + if (v) { + setProjResWizVisible(v); + } else { + waitForSave(undefined, 200).then(() => { + setProjResWizVisible(v); + projMediaRef.current = undefined; + setVisual(false); + }); + } + }; + + const handleAllResources = () => { + setAllResources(!allResources); + }; + + const handleEdit = (id: string) => { + const secRes = sectionResources.find( + (r) => related(r, 'mediafile') === id + ) as SectionResourceD; + setEditResource(secRes); + setAllowEditSave(true); + resourceTypeRef.current = related(secRes, 'passage') + ? ResourceTypeEnum.passageResource + : ResourceTypeEnum.sectionResource; + descriptionRef.current = secRes?.attributes.description || ''; + const mf = mediafiles.find((m) => m.id === related(secRes, 'mediafile')); + catIdRef.current = mf ? related(mf, 'artifactCategory') : undefined; + mediaRef.current = mf as MediaFileD; + const ct = mediaContentType(mf); + setUploadType( + ct === MarkDownType + ? UploadType.MarkDown + : ct === UriLinkType + ? UploadType.Link + : UploadType.Resource + ); + }; + const resetEdit = () => { + setEditResource(undefined); + catIdRef.current = undefined; + descriptionRef.current = ''; + resourceTypeRef.current = ResourceTypeEnum.sectionResource; + setUploadVisible(false); + setMarkdownValue(''); + setInitDescription(''); + setAIGenerated(false); + }; + const handleEditResourceVisible = (v: boolean) => { + if (!v) resetEdit(); + }; + const handleEditSave = async () => { + if (editResource) { + UpdateSectionResource({ + ...editResource, + attributes: { + ...editResource.attributes, + description: descriptionRef.current, + }, + }); + if (Boolean(related(editResource, 'passage')) !== isPassageResource()) { + await memory.update((t) => [ + ...ReplaceRelatedRecord( + t, + editResource, + 'passage', + 'passage', + isPassageResource() ? passage.id : '' + ), + ]); + } + const mf = mediafiles.find( + (m) => m.id === related(editResource, 'mediafile') + ) as MediaFileD | undefined; + if (mf && textRef.current) { + await memory.update((t) => [ + t.replaceAttribute(mf, 'originalFile', textRef.current), + ]); + } + if (mf && catIdRef.current) { + await memory.update((t) => [ + ...ReplaceRelatedRecord( + t, + mf, + 'artifactCategory', + 'artifactcategory', + catIdRef.current + ), + ]); + } + if (mf && isPassageResource() !== Boolean(related(mf, 'passage'))) { + await memory.update((t) => [ + ...ReplaceRelatedRecord( + t, + mf, + 'passage', + 'passage', + isPassageResource() ? passage.id : '' + ), + ]); + } + } + resetEdit(); + }; + const handleEditCancel = () => { + resetEdit(); + }; + const handleAction = (what: string) => { + artifactState.id = resourceType ?? null; + resourceTypeRef.current = ResourceTypeEnum.sectionResource; + if (what === 'upload') { + mediaRef.current = undefined; + setUploadType(UploadType.Resource); + setRecordAudio(false); + setUploadVisible(true); + } else if (what === 'scripture') { + setAudioScriptureVisible(true); + } else if (what === 'link') { + setUploadType(UploadType.Link); + setRecordAudio(false); + setUploadVisible(true); + } else if (what === 'record') { + setUploadType(UploadType.Resource); + setRecordAudio(true); + setUploadVisible(true); + } else if (what === 'text') { + setUploadType(UploadType.MarkDown); + setRecordAudio(false); + setUploadVisible(true); + } else if (what === 'shared') { + resourceTypeRef.current = ResourceTypeEnum.sectionResource; + setSharedResourceVisible(true); + } + }; + + const handleMarkdownValue = ( + query: string, + audioUrl: string, + transcript: string + ) => { + setInitDescription(query); + descriptionRef.current = query; + if (audioUrl) { + setUploadType(UploadType.FaithbridgeLink); + } else { + setUploadType(UploadType.MarkDown); + } + setMarkdownValue(audioUrl ? `${audioUrl}||${transcript}` : transcript); + setRecordAudio(false); + setAIGenerated(true); + setUploadVisible(true); + }; + + const passDesc = useMemo( + () => + passageTypeFromRef(passage?.attributes?.reference) === + PassageTypeEnum.NOTE + ? t.noteResource + : t.passageResource, + // eslint-disable-next-line react-hooks/exhaustive-deps + [passage] + ); + + const getSectionType = () => { + const level = section.attributes?.level; + if (level === SheetLevel.Book) return 'BOOK'; + if (level === SheetLevel.Movement) return 'MOVE'; + return undefined; + }; + + const sectDesc = useMemo( + () => + getSectionType() === PassageTypeEnum.BOOK + ? t.bookResource + : getSectionType() === PassageTypeEnum.MOVEMENT + ? t.movementResource + : getOrganizedBy(true), + // eslint-disable-next-line react-hooks/exhaustive-deps + [section] + ); + + const listFilter = (r: IRow) => + r?.isResource && + (allResources || r.passageId === '' || r.passageId === passage.id); + + const [selectedRows, setSelectedRows] = useState( + rowData.filter(listFilter) + ); + + useEffect(() => { + if (!busy && !remoteBusy) { + setSelectedRows(rowData.filter(listFilter)); + setSortKey((sortKey) => sortKey + 1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rowData, allResources, busy, remoteBusy]); + + const onSortEnd = ({ + oldIndex, + newIndex, + }: { + oldIndex: number; + newIndex: number; + }) => { + if (oldIndex === newIndex) return; + const indexes = Array(); + rowData.forEach((r, i) => { + if (listFilter(r)) indexes.push(i); + }); + const newIndexes = arrayMove(indexes, oldIndex, newIndex) as number[]; + for (let i = 0; i < newIndexes.length; i += 1) { + const secResRec = sectionResources.find( + (r) => + related(r, 'mediafile') === (rowData[newIndexes[i] ?? 0] as IRow).id + ); + if (secResRec && secResRec.attributes?.sequenceNum !== i) { + UpdateSectionResource({ + ...secResRec, + attributes: { ...secResRec?.attributes, sequenceNum: i }, + }); + } + } + const newRows = rowData + .map((r, i) => (listFilter(r) ? rowData[newIndexes[i] ?? 0] : r)) + .filter((r) => r !== undefined); + forceRefresh(newRows); + }; + + const afterUpload = async (planId: string, mediaRemoteIds?: string[]) => { + let cnt = rowData.length; + const projRes = new Array(); + if (mediaRemoteIds && mediaRemoteIds.length > 0) { + for (const remId of mediaRemoteIds) { + cnt += 1; + const id = + remoteIdGuid('mediafile', remId, memory?.keyMap as RecordKeyMap) || + remId; + const mediaRecId = { type: 'mediafile', id }; + if (descriptionRef.current) { + await memory.update((t) => [ + t.replaceAttribute(mediaRecId, 'topic', descriptionRef.current), + ]); + } + if (catIdRef.current) { + await memory.update((t) => [ + ...ReplaceRelatedRecord( + t, + mediaRecId, + 'artifactCategory', + 'artifactcategory', + catIdRef.current + ), + ]); + } + if (isPassageResource()) { + await memory.update((t) => [ + ...ReplaceRelatedRecord( + t, + mediaRecId, + 'passage', + 'passage', + passage.id + ), + ]); + } + if (!isProjectResource()) { + await AddSectionResource( + cnt, + descriptionRef.current, + mediaRecId, + isPassageResource() ? passage.id : null + ); + } else { + projRes.push(findRecord(memory, 'mediafile', id) as MediaFileD); + } + } + if (projRes.length === 1) setProjResSetup(projRes); + resetEdit(); + } + }; + + const resourceSourcePassages = useMemo(() => { + const results: number[] = []; + sectionResources.forEach((sr) => { + const rec = findRecord(memory, 'mediafile', related(sr, 'mediafile')) as + | MediaFileD + | undefined; + if (rowData.find((r) => r.id === rec?.id)) { + const passageId = rec?.attributes.resourcePassageId; + if (passageId) results.push(passageId); + } + }); + return results; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sectionResources, rowData]); + + useEffect(() => { + getArtifactCategorys(ArtifactCategoryType.Resource).then( + (cats) => (catRef.current = cats) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSelectShared = async (res: Resource[]) => { + let cnt = rowData.length; + for (const r of res) { + const catRec = catRef.current.find( + (c) => c.slug === r?.attributes?.categoryName + ); + const newMediaRec = await AddMediaFileResource(r, catRec?.id || ''); + cnt += 1; + await AddSectionResource( + cnt, + r?.attributes?.title || r?.attributes?.reference, + newMediaRec, + isPassageResource() ? passage.id : null + ); + } + }; + + const handleSelectProjectResource = (m: MediaFileD) => { + setSelected(m.id, PlayInPlayer.yes); + projMediaRef.current = m; + setVisual(isVisual(m)); + setProjectResourceVisible(false); + setProjResPassageVisible(true); + }; + + const writeVisualResource = async (items: RecordIdentity[]) => { + const t = new RecordTransformBuilder(); + let cnt = 0; + const total = items.length; + for (const i of items) { + const rec = memory.cache.query((q) => q.findRecord(i)) as + | Passage + | Section; + const secRec = + rec?.type === 'section' + ? (rec as Section) + : (memory.cache.query((q) => + q.findRecord({ type: 'section', id: related(rec, 'section') }) + ) as Section); + const secNum = secRec?.attributes.sequencenum || 0; + const topicIn = + projMediaRef.current?.attributes?.topic || + removeExtension(projMediaRef.current?.attributes?.originalFile || '') + ?.name; + const passage = rec?.type === 'passage' ? (rec as Passage) : undefined; + await projectResourceSave({ + t, + media: projMediaRef.current as MediaFile, + i: { secNum, section: secRec, passage }, + topicIn, + limitValue: '', + mediafiles, + sectionResources, + }); + cnt += 1; + setComplete(Math.min((cnt * 100) / total, 100)); + } + // Ensure setComplete(0) is always called after processing + setComplete(0); + }; + + const handleTextChange = (text: string) => { + textRef.current = text; + const ct = mediaContentType(mediaRef.current); + if (ct === MarkDownType) { + const newAllow = text.trim() !== ''; + if (allowEditSave !== newAllow) { + setAllowEditSave(newAllow); + } + return; + } + const validUrl = isUrl(text); + if (ct === UriLinkType) { + if (allowEditSave !== validUrl) { + setAllowEditSave(validUrl); + } + } + }; + + useEffect(() => { + if (!projResPassageVisible && !projResWizVisible && projMediaRef.current) + setProjResSetup(projResSetup.filter((m) => m !== projMediaRef.current)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projResPassageVisible, projResWizVisible]); + + useEffect(() => { + if (projResSetup.length) { + handleSelectProjectResource(projResSetup[0] as MediaFileD); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projResSetup]); + + const handleSelectProjectResourcePassage = (items: RecordIdentity[]) => { + projIdentRef.current = items; + if (isVisual(projMediaRef.current)) { + writeVisualResource(items).then(() => { + setProjResPassageVisible(false); + }); + } else { + setProjResWizVisible(true); + setProjResPassageVisible(false); + } + }; + + const handleCategory = (categoryId: string) => { + catIdRef.current = categoryId; + }; + + const handleDescription = (desc: string) => { + descriptionRef.current = desc; + }; + + const handlePassRes = (newValue: ResourceTypeEnum) => { + resourceTypeRef.current = newValue; + if (isProjectResource()) { + artifactState.id = projResourceType ?? null; + setUploadType(UploadType.ProjectResource); + } else if ( + artifactState.id === projResourceType || + uploadType === UploadType.ProjectResource + ) { + artifactState.id = resourceType ?? null; + setUploadType(UploadType.Resource); + } + }; + + const handleEnded = () => { + setPlayItem(''); + handleItemPlayEnd(); + }; + + const handleLoaded = () => { + if (playItem !== '' && !itemPlaying) { + setTimeout(() => handleItemTogglePlay(), 1000); + } + }; + + const [hasProjRes, setHasProjRes] = useState(false); + + useEffect(() => { + getProjectResources().then((res) => setHasProjRes(res.length > 0)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mediafiles]); + + const isScripture = useMemo( + () => planType(plan)?.scripture, + // eslint-disable-next-line react-hooks/exhaustive-deps + [plan] + ); + + const modifiable = useMemo( + () => hasPermission && (!offline || offlineOnly), + [hasPermission, offline, offlineOnly] + ); + + return ( + <> + + + {isScripture && ( + + handleFindVisible(true)}> + {t.research} + + + )} + {hasPermission && (!offline || offlineOnly) && ( + <> + + + + {hasProjRes && ( + + setProjectResourceVisible(true)}> + {t.configure} + + + )} + + )} + {playItem !== '' && ( + + + + + + )} + {otherResourcesAvailable && ( + + + + )} + + + + + {selectedRows.map((value, index) => ( + + ))} + + true} + onNonAudio={handleNonAudio} + performedBy={performedBy} + onSpeakerChange={(value) => setPerformedBy(value)} + inValue={markdownValue} + eafUrl={aiGenerated ? AIGenerated : ''} + defaultFilename={filename} + metaData={ + + } + /> + {t.findResourceDesc}} + isOpen={findOpen} + onOpen={handleFindVisible} + bp={BigDialogBp.sm} + > + handleFindVisible(false)} + canAdd={hasPermission} + onMarkdown={handleMarkdownValue} + /> + + + (resourceTypeRef.current = val)} + onSelect={handleSelectShared} + onOpen={handleSharedResourceVisible} + /> + + + + + + {projResPassageVisible ? ( + + ) : ( + <> + )} + + + {projResWizVisible ? ( + + ) : ( + <> + )} + + + + + {confirm && ( + + )} + {displayId && ( + + )} + {markDown && ( + setMarkDoan('')} + bp={BigDialogBp.sm} + > + + + )} + {audioScriptureVisible && ( + setAudioScriptureVisible(false)} + bp={BigDialogBp.sm} + setCloseRequested={setBiblebrainClose} + > + setAudioScriptureVisible(false)} + closeRequested={biblebrainClose} + /> + + )} + setLink('')} /> + + ); +} + +export default PassageDetailArtifactsMobile; diff --git a/src/renderer/src/routes/PassageDetail.tsx b/src/renderer/src/routes/PassageDetail.tsx index da8e33b9..82727f15 100644 --- a/src/renderer/src/routes/PassageDetail.tsx +++ b/src/renderer/src/routes/PassageDetail.tsx @@ -47,6 +47,9 @@ import PassageDetailParatextIntegration from '../components/PassageDetail/Passag import { PassageDetailDiscuss } from '../components/PassageDetail/PassageDetailDiscuss'; import { addPt } from '../utils/addPt'; import DiscussionPanel from '../components/Discussions/DiscussionPanel'; +import PassageDetailMobileDetail from '../components/PassageDetail/PassageDetailMobileDetail'; +import PassageDetailsArtifactsMobile from '../components/PassageDetail/Internalization/PassageDetailsArtifactsMobile'; + const KeyTerms = React.lazy( () => import('../components/PassageDetail/Keyterms/KeyTerms') @@ -433,6 +436,17 @@ export const PassageDetail = () => { const [user] = useGlobal('user'); const { setProjectType } = useProjectType(); + const { isMobile } = useMobile(); + const [paneWidth, setPaneWidth] = useState(0); + + useEffect(() => { + if (isMobile) { + setPaneWidth(window.innerWidth); + } else { + setPaneWidth(window.innerWidth - 450); // Start with discussion panel open on desktop + } + }, [isMobile]); + useEffect(() => { const projectId = setUrlContext(prjId ?? ''); if (user && projType === '') { @@ -460,7 +474,19 @@ export const PassageDetail = () => { > - + { isMobile ? ( + + {/* */} + + } + noAudioText={''} + /> + ) : ( + + )} ); From 66a9a7d3366cb4d5910e8e00a2d427c79a2b6bcc Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Mon, 23 Mar 2026 15:53:00 -0700 Subject: [PATCH 03/20] migrate sortable items to prelim resource cards --- .../PassageDetailsArtifactsMobile.tsx | 41 +++++--- .../PassageDetail/Internalization/index.ts | 1 + .../mobile components/AudioResourceCard.tsx | 97 +++++++++++++++++++ .../mobile components/TextResourceCard.tsx | 89 +++++++++++++++++ 4 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx create mode 100644 src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index bf8054f7..cbe2c9de 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -20,7 +20,9 @@ import AddResource from './AddResource'; import SortableHeader from './SortableHeader'; import { IRow } from '../../../context/PassageDetailContext'; import { AltButton } from '../../../control'; -import { AIGenerated, SortableItem } from '.'; +import { AIGenerated } from '.'; +import { AudioResourceCard } from './mobile components/AudioResourceCard'; +import { TextResourceCard } from './mobile components/TextResourceCard'; import { remoteIdGuid, useSecResCreate, @@ -860,19 +862,30 @@ export function PassageDetailArtifactsMobile() { {selectedRows.map((value, index) => ( - + /^audio/.test(mediaContentType(value.mediafile)) ? ( + + ) : ( + + ) ))} void; + onDone?: (id: string, res: SectionResourceD | null) => void; + onEnded?: () => void; + subtitle?: string; + limits?: { + start?: number; + end?: number; + }; + sx?: SxProps; +} + +export function AudioResourceCard({ + row, + isPlaying, + onPlay, + onDone, + onEnded, + subtitle = 'Scripture', + limits, + sx, +}: IProps) { + const handleDoneToggle = () => { + if (onDone) { + onDone(row.id, row.resource); + } + }; + + return ( + + + + + {row.artifactName} + + + {subtitle} + + + + + + {/* Audio playback UI for audio/* resource files. */} + {})} + onTogglePlay={() => onPlay(row.id)} + controls + limits={limits ?? {}} + noClose + noRestart + noSkipBack + sx={{ borderRadius: 1, bgcolor: 'grey.100' }} + /> + + + ); +} + +export default AudioResourceCard; diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx new file mode 100644 index 00000000..1707ca65 --- /dev/null +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx @@ -0,0 +1,89 @@ +import { Box, Card, Checkbox, IconButton, SxProps, Typography } from '@mui/material'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import { IRow } from '../../../../context/PassageDetailContext'; +import { SectionResourceD } from '../../../../model'; + +// This card is used for non-audio resources in the mobile list. +// It covers markdown text, URI links, PDFs, images, and other file types +// that are not routed to the audio player card. + +interface IProps { + row: IRow; + onView: (id: string) => void; + onDone?: (id: string, res: SectionResourceD | null) => void; + subtitle?: string; + sx?: SxProps; +} + +export function TextResourceCard({ + row, + onView, + onDone, + subtitle = 'Translation Resource', + sx, +}: IProps) { + const handleDoneToggle = () => { + if (onDone) { + onDone(row.id, row.resource); + } + }; + + return ( + + + + + {row.artifactName} + + + {subtitle} + + + + + + + onView(row.id)} + aria-label={`View ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + + + ); +} + +export default TextResourceCard; From 066337b7e1bfa4d2d795bc8eb2ae42c9725994a5 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Mon, 23 Mar 2026 15:56:35 -0700 Subject: [PATCH 04/20] remove sortable header, delete file later --- .../Internalization/PassageDetailsArtifactsMobile.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index cbe2c9de..7864181d 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -17,7 +17,6 @@ import { PlayInPlayer } from '../../../context/PlayInPlayer'; import { useSnackBar } from '../../../hoc/SnackBar'; import Uploader from '../../Uploader'; import AddResource from './AddResource'; -import SortableHeader from './SortableHeader'; import { IRow } from '../../../context/PassageDetailContext'; import { AltButton } from '../../../control'; import { AIGenerated } from '.'; @@ -859,7 +858,6 @@ export function PassageDetailArtifactsMobile() { )} - {selectedRows.map((value, index) => ( /^audio/.test(mediaContentType(value.mediafile)) ? ( From f96d6fef482597367b37b0be03c0b43fba6340c6 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Mon, 23 Mar 2026 16:18:33 -0700 Subject: [PATCH 05/20] fix box width --- .../PassageDetailsArtifactsMobile.tsx | 58 ++++++++++--------- .../mobile components/AudioResourceCard.tsx | 12 +++- .../mobile components/TextResourceCard.tsx | 10 +++- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index 7864181d..06166563 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -858,34 +858,36 @@ export function PassageDetailArtifactsMobile() { )} - - {selectedRows.map((value, index) => ( - /^audio/.test(mediaContentType(value.mediafile)) ? ( - - ) : ( - - ) - ))} - + + + {selectedRows.map((value, index) => ( + + {/^audio/.test(mediaContentType(value.mediafile)) ? ( + + ) : ( + + )} + + ))} + + - - + + {row.artifactName} @@ -75,7 +81,7 @@ export function AudioResourceCard({ }} /> - + {/* Audio playback UI for audio/* resource files. */} - - + + {row.artifactName} From 71c04f4d38df6168948d5a2c9d7c27373f0a1576 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Mon, 23 Mar 2026 16:53:28 -0700 Subject: [PATCH 06/20] small layout fixes --- .../PassageDetailsArtifactsMobile.tsx | 2 +- .../mobile components/AudioResourceCard.tsx | 76 +++++++++++-------- .../mobile components/TextResourceCard.tsx | 75 ++++++++++-------- 3 files changed, 87 insertions(+), 66 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index 06166563..a7feacf2 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -858,7 +858,7 @@ export function PassageDetailArtifactsMobile() { )} - + {selectedRows.map((value, index) => ( diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx index f56797ca..c2a12112 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx @@ -57,44 +57,54 @@ export function AudioResourceCard({ > - - - {row.artifactName} - - - {subtitle} - - - - - - {/* Audio playback UI for audio/* resource files. */} - {})} - onTogglePlay={() => onPlay(row.id)} - controls - limits={limits ?? {}} - noClose - noRestart - noSkipBack - sx={{ borderRadius: 1, bgcolor: 'grey.100' }} - /> + > + + + {row.artifactName} + + + + + + {subtitle} + + + {/* Audio playback UI for audio/* resource files. */} + {})} + onTogglePlay={() => onPlay(row.id)} + controls + limits={limits ?? {}} + noClose + noRestart + noSkipBack + sx={{ borderRadius: 1, bgcolor: 'grey.100' }} + /> + ); diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx index d7ac4285..068f891e 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx @@ -49,44 +49,55 @@ export function TextResourceCard({ > - - - {row.artifactName} - - - {subtitle} - - - - - - - onView(row.id)} - aria-label={`View ${row.artifactName}`} - sx={{ p: 0.25 }} > - - + + + {row.artifactName} + + + + + + + {subtitle} + + + + onView(row.id)} + aria-label={`View ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + ); From b8c3a18f6b76ec9b8cd37c4e30767923dc787e81 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Tue, 24 Mar 2026 10:13:47 -0700 Subject: [PATCH 07/20] audio and card drag interfer fix --- .../PassageDetailsArtifactsMobile.tsx | 44 +++------------- src/renderer/src/hoc/VertListDnd.tsx | 51 ++++++++++++++++++- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index a7feacf2..9d1d1ac3 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -49,14 +49,11 @@ import SelectProjectResource from './SelectProjectResource'; import SelectSections from './SelectSections'; import ResourceData from './ResourceData'; import { MarkDownType, UriLinkType } from '../../MediaUpload'; -import LimitedMediaPlayer from '../../LimitedMediaPlayer'; import { Badge, Box, - BoxProps, Grid, Stack, - styled, Typography, } from '@mui/material'; import { ReplaceRelatedRecord } from '../../../model/baseModel'; @@ -99,17 +96,6 @@ import { MarkDownView } from '../../../control/MarkDownView'; import { UploadType } from '../../UploadType'; import { ResourceTypeEnum } from './ResourceTypeEnum'; -const MediaContainer = styled(Box)(({ theme }) => ({ - marginRight: theme.spacing(2), - marginTop: theme.spacing(1), - width: '100%', - '& audio': { - height: '40px', - display: 'flex', - width: 'inherit', - }, -})); - export function PassageDetailArtifactsMobile() { const sectionResources = useOrbitData('sectionresource'); const mediafiles = useOrbitData('mediafile'); @@ -134,7 +120,6 @@ export function PassageDetailArtifactsMobile() { toggleDone, forceRefresh, handleItemPlayEnd, - handleItemTogglePlay, getProjectResources, } = usePassageDetailContext(); const { getOrganizedBy } = useOrganizedBy(); @@ -779,12 +764,6 @@ export function PassageDetailArtifactsMobile() { handleItemPlayEnd(); }; - const handleLoaded = () => { - if (playItem !== '' && !itemPlaying) { - setTimeout(() => handleItemTogglePlay(), 1000); - } - }; - const [hasProjRes, setHasProjRes] = useState(false); useEffect(() => { @@ -832,21 +811,6 @@ export function PassageDetailArtifactsMobile() { )} )} - {playItem !== '' && ( - - - - - - )} {otherResourcesAvailable && ( - + {selectedRows.map((value, index) => ( {/^audio/.test(mediaContentType(value.mediafile)) ? ( diff --git a/src/renderer/src/hoc/VertListDnd.tsx b/src/renderer/src/hoc/VertListDnd.tsx index b6099f9d..17977c78 100644 --- a/src/renderer/src/hoc/VertListDnd.tsx +++ b/src/renderer/src/hoc/VertListDnd.tsx @@ -5,7 +5,7 @@ import { Draggable, DropResult, } from '@hello-pangea/dnd'; -import { List, ListItem } from '@mui/material'; +import { Box, List, ListItem } from '@mui/material'; export interface DropProp { id: string; @@ -34,6 +34,8 @@ export interface OnDropProps { export interface VertListDndProps extends PropsWithChildren { data?: DropProp[]; dragHandle?: boolean; + lockHorizontal?: boolean; + dragHandleRegion?: 'full' | 'top-half'; onDrop?: (props: OnDropProps) => void; } @@ -41,6 +43,8 @@ export const VertListDnd = ({ data, onDrop, dragHandle, + lockHorizontal, + dragHandleRegion = 'full', children, }: VertListDndProps) => { const [items, setItems] = useState( @@ -79,6 +83,28 @@ export const VertListDnd = ({ } }; + const lockTransformToVertical = (style: any) => { + if (!lockHorizontal || !style?.transform) return style; + const transform = String(style.transform); + if (transform.startsWith('translate3d(')) { + // translate3d(xpx, ypx, zpx) -> lock x to 0px + style.transform = transform.replace( + /translate3d\([^,]+,\s*([^,]+),\s*([^\)]+)\)/, + 'translate3d(0px, $1, $2)' + ); + return style; + } + if (transform.startsWith('translate(')) { + // translate(xpx, ypx) -> lock x to 0px + style.transform = transform.replace( + /translate\([^,]+,\s*([^\)]+)\)/, + 'translate(0px, $1)' + ); + return style; + } + return style; + }; + return ( @@ -97,8 +123,12 @@ export const VertListDnd = ({ + {dragHandleRegion === 'top-half' && ( + + )} {item.content} )} From 7e689f9e223451a9f00850c271f3b15a1cb98d5c Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Tue, 24 Mar 2026 10:40:15 -0700 Subject: [PATCH 08/20] playbutton and show category fix --- src/renderer/src/components/LimitedMediaPlayer.tsx | 9 +++++++-- .../Internalization/PassageDetailsArtifactsMobile.tsx | 2 ++ .../mobile components/AudioResourceCard.tsx | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/LimitedMediaPlayer.tsx b/src/renderer/src/components/LimitedMediaPlayer.tsx index 712286df..2d543ec7 100644 --- a/src/renderer/src/components/LimitedMediaPlayer.tsx +++ b/src/renderer/src/components/LimitedMediaPlayer.tsx @@ -73,6 +73,7 @@ interface IProps { sx?: SxProps; noRestart?: boolean; noSkipBack?: boolean; + playButtonSize?: 'small' | 'medium' | 'large'; } export function LimitedMediaPlayer(props: IProps) { @@ -88,6 +89,7 @@ export function LimitedMediaPlayer(props: IProps) { sx, noRestart, noSkipBack, + playButtonSize = 'small', } = props; const [value, setValue] = useState(0); const [ready, setReady] = useState(false); @@ -295,10 +297,13 @@ export function LimitedMediaPlayer(props: IProps) { onClick={handlePlayPause} > {playing ? ( - + ) : ( )} diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index 9d1d1ac3..08a3f9a9 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -835,6 +835,7 @@ export function PassageDetailArtifactsMobile() { {/^audio/.test(mediaContentType(value.mediafile)) ? ( From b61a225a17469fa31cc8f910822afc174f07efc1 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Tue, 24 Mar 2026 12:30:05 -0700 Subject: [PATCH 09/20] add delete button to cards --- .../PassageDetailsArtifactsMobile.tsx | 2 + .../mobile components/AudioResourceCard.tsx | 45 ++++++++++++------- .../mobile components/TextResourceCard.tsx | 16 ++++++- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index 08a3f9a9..5e2cbd17 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -839,6 +839,7 @@ export function PassageDetailArtifactsMobile() { isPlaying={playItem === value.id && itemPlaying} onPlay={handlePlay} onDone={handleDone} + onDelete={modifiable ? handleDelete : undefined} onEnded={handleEnded} limits={{ start: mediaStart ?? 0, end: mediaEnd ?? 0 }} /> @@ -854,6 +855,7 @@ export function PassageDetailArtifactsMobile() { : handleDisplayId } onDone={handleDone} + onDelete={modifiable ? handleDelete : undefined} /> )} diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx index 35e02fc6..b69bd8a6 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx @@ -1,4 +1,5 @@ -import { Box, Card, Checkbox, SxProps, Typography } from '@mui/material'; +import { Box, Card, Checkbox, IconButton, SxProps, Typography } from '@mui/material'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { IRow } from '../../../../context/PassageDetailContext'; import { SectionResourceD } from '../../../../model'; import LimitedMediaPlayer from '../../../LimitedMediaPlayer'; @@ -12,6 +13,7 @@ interface IProps { isPlaying: boolean; onPlay: (id: string) => void; onDone?: (id: string, res: SectionResourceD | null) => void; + onDelete?: (id: string) => void; onEnded?: () => void; subtitle?: string; limits?: { @@ -26,6 +28,7 @@ export function AudioResourceCard({ isPlaying, onPlay, onDone, + onDelete, onEnded, subtitle = 'Scripture', limits, @@ -90,21 +93,33 @@ export function AudioResourceCard({ {subtitle} - + {/* Audio playback UI for audio/* resource files. */} - {})} - onTogglePlay={() => onPlay(row.id)} - controls - limits={limits ?? {}} - noClose - noRestart - noSkipBack - playButtonSize="large" - sx={{ borderRadius: 1, bgcolor: 'grey.100' }} - /> + + {})} + onTogglePlay={() => onPlay(row.id)} + controls + limits={limits ?? {}} + noClose + noRestart + noSkipBack + playButtonSize="large" + sx={{ borderRadius: 1, bgcolor: 'grey.100' }} + /> + + {onDelete && ( + onDelete(row.id)} + aria-label={`Delete ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + )} diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx index 068f891e..5620c2cc 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx @@ -1,5 +1,6 @@ import { Box, Card, Checkbox, IconButton, SxProps, Typography } from '@mui/material'; import VisibilityIcon from '@mui/icons-material/Visibility'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { IRow } from '../../../../context/PassageDetailContext'; import { SectionResourceD } from '../../../../model'; @@ -11,6 +12,7 @@ interface IProps { row: IRow; onView: (id: string) => void; onDone?: (id: string, res: SectionResourceD | null) => void; + onDelete?: (id: string) => void; subtitle?: string; sx?: SxProps; } @@ -19,6 +21,7 @@ export function TextResourceCard({ row, onView, onDone, + onDelete, subtitle = 'Translation Resource', sx, }: IProps) { @@ -84,7 +87,7 @@ export function TextResourceCard({ {subtitle} - + + {onDelete && ( + onDelete(row.id)} + aria-label={`Delete ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + )} ); } - export default TextResourceCard; From 412cf75addbadf76665cc118b0484a6ea8cea839 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Tue, 24 Mar 2026 13:21:51 -0700 Subject: [PATCH 10/20] add edit button --- .../PassageDetailsArtifactsMobile.tsx | 2 ++ .../mobile components/AudioResourceCard.tsx | 13 +++++++ .../mobile components/TextResourceCard.tsx | 35 +++++++++++++------ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx index 5e2cbd17..d3919bc7 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/PassageDetailsArtifactsMobile.tsx @@ -839,6 +839,7 @@ export function PassageDetailArtifactsMobile() { isPlaying={playItem === value.id && itemPlaying} onPlay={handlePlay} onDone={handleDone} + onEdit={modifiable ? handleEdit : undefined} onDelete={modifiable ? handleDelete : undefined} onEnded={handleEnded} limits={{ start: mediaStart ?? 0, end: mediaEnd ?? 0 }} @@ -855,6 +856,7 @@ export function PassageDetailArtifactsMobile() { : handleDisplayId } onDone={handleDone} + onEdit={modifiable ? handleEdit : undefined} onDelete={modifiable ? handleDelete : undefined} /> )} diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx index b69bd8a6..9ae07278 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx @@ -1,5 +1,6 @@ import { Box, Card, Checkbox, IconButton, SxProps, Typography } from '@mui/material'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import EditIcon from '@mui/icons-material/Edit'; import { IRow } from '../../../../context/PassageDetailContext'; import { SectionResourceD } from '../../../../model'; import LimitedMediaPlayer from '../../../LimitedMediaPlayer'; @@ -13,6 +14,7 @@ interface IProps { isPlaying: boolean; onPlay: (id: string) => void; onDone?: (id: string, res: SectionResourceD | null) => void; + onEdit?: (id: string) => void; onDelete?: (id: string) => void; onEnded?: () => void; subtitle?: string; @@ -28,6 +30,7 @@ export function AudioResourceCard({ isPlaying, onPlay, onDone, + onEdit, onDelete, onEnded, subtitle = 'Scripture', @@ -110,6 +113,16 @@ export function AudioResourceCard({ sx={{ borderRadius: 1, bgcolor: 'grey.100' }} /> + {onEdit && ( + onEdit(row.id)} + aria-label={`Edit ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + )} {onDelete && ( void; onDone?: (id: string, res: SectionResourceD | null) => void; + onEdit?: (id: string) => void; onDelete?: (id: string) => void; subtitle?: string; sx?: SxProps; @@ -21,6 +23,7 @@ export function TextResourceCard({ row, onView, onDone, + onEdit, onDelete, subtitle = 'Translation Resource', sx, @@ -100,16 +103,28 @@ export function TextResourceCard({ > - {onDelete && ( - onDelete(row.id)} - aria-label={`Delete ${row.artifactName}`} - sx={{ p: 0.25 }} - > - - - )} + + {onEdit && ( + onEdit(row.id)} + aria-label={`Edit ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + )} + {onDelete && ( + onDelete(row.id)} + aria-label={`Delete ${row.artifactName}`} + sx={{ p: 0.25 }} + > + + + )} + From 8be6cee19c579884a65f93c8c748637e54b6a2ef Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Tue, 24 Mar 2026 14:16:04 -0700 Subject: [PATCH 11/20] remove media player container and spacing fix --- .../src/components/LimitedMediaPlayer.tsx | 98 +++++++++++++------ .../PassageDetailsArtifactsMobile.tsx | 3 + .../mobile components/AudioResourceCard.tsx | 2 +- .../mobile components/TextResourceCard.tsx | 2 +- src/renderer/src/hoc/VertListDnd.tsx | 12 ++- 5 files changed, 82 insertions(+), 35 deletions(-) diff --git a/src/renderer/src/components/LimitedMediaPlayer.tsx b/src/renderer/src/components/LimitedMediaPlayer.tsx index 2d543ec7..66f316cb 100644 --- a/src/renderer/src/components/LimitedMediaPlayer.tsx +++ b/src/renderer/src/components/LimitedMediaPlayer.tsx @@ -74,6 +74,7 @@ interface IProps { noRestart?: boolean; noSkipBack?: boolean; playButtonSize?: 'small' | 'medium' | 'large'; + noContainer?: boolean; } export function LimitedMediaPlayer(props: IProps) { @@ -90,6 +91,7 @@ export function LimitedMediaPlayer(props: IProps) { noRestart, noSkipBack, playButtonSize = 'small', + noContainer, } = props; const [value, setValue] = useState(0); const [ready, setReady] = useState(false); @@ -258,8 +260,24 @@ export function LimitedMediaPlayer(props: IProps) { {!controls ? ( <> ) : ( - { + const playPauseButtonSx = noContainer + ? { + alignSelf: 'center', + color: 'text.primary', + m: 0, + p: 0.5, + } + : { + alignSelf: 'center', + color: 'text.primary', + m: '0!important', + pb: '0!important', + pt: '7px!important', + pr: '0', + }; + + const mediaControls = ( <> {!noRestart && ( @@ -286,14 +304,7 @@ export function LimitedMediaPlayer(props: IProps) { {playing ? ( @@ -310,9 +321,16 @@ export function LimitedMediaPlayer(props: IProps) { - } - label={ - + ); + + const mediaLabel = ( + - } - deleteIcon={ - !noClose ? ( - - - - - - ) : ( - <> - ) - } - sx={{ ...sx, width: '100%' }} - /> + ); + + const closeControl = !noClose ? ( + + + + + + ) : ( + <> + ); + + return noContainer ? ( + + + {mediaControls} + + {mediaLabel} + {closeControl} + + ) : ( + + ); + })() )} {selectedRows.map((value, index) => ( diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx index 9ae07278..454fb0ac 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/AudioResourceCard.tsx @@ -110,7 +110,7 @@ export function AudioResourceCard({ noRestart noSkipBack playButtonSize="large" - sx={{ borderRadius: 1, bgcolor: 'grey.100' }} + noContainer /> {onEdit && ( diff --git a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx index a7ff548c..5e31a618 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/mobile components/TextResourceCard.tsx @@ -48,7 +48,7 @@ export function TextResourceCard({ borderColor: 'grey.700', borderRadius: 2, backgroundColor: 'background.paper', - px: 1.25, + px: 0.5, py: 1, ...sx, }} diff --git a/src/renderer/src/hoc/VertListDnd.tsx b/src/renderer/src/hoc/VertListDnd.tsx index 17977c78..7158f35b 100644 --- a/src/renderer/src/hoc/VertListDnd.tsx +++ b/src/renderer/src/hoc/VertListDnd.tsx @@ -36,6 +36,9 @@ export interface VertListDndProps extends PropsWithChildren { dragHandle?: boolean; lockHorizontal?: boolean; dragHandleRegion?: 'full' | 'top-half'; + itemSpacing?: number; + listPaddingX?: number; + itemPaddingX?: number; onDrop?: (props: OnDropProps) => void; } @@ -45,6 +48,9 @@ export const VertListDnd = ({ dragHandle, lockHorizontal, dragHandleRegion = 'full', + itemSpacing = 1, + listPaddingX, + itemPaddingX, children, }: VertListDndProps) => { const [items, setItems] = useState( @@ -114,7 +120,8 @@ export const VertListDnd = ({ ref={provided.innerRef} sx={{ bgcolor: snapshot.isDraggingOver ? 'secondary.light' : 'white', - p: 1, + py: 1, + px: listPaddingX ?? 1, }} > {items.map((item, index) => ( @@ -134,7 +141,8 @@ export const VertListDnd = ({ : dragHandle ? 'transparent' : 'lightgrey', - mb: 1, + mb: itemSpacing, + ...(itemPaddingX !== undefined ? { px: itemPaddingX } : {}), }} > {dragHandleRegion === 'top-half' && ( From ebdf4ab1ffa397cd0f1f0a10530c9ab0cb627365 Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Tue, 24 Mar 2026 16:27:40 -0700 Subject: [PATCH 12/20] update research and add button styling --- .../Internalization/AddResource.tsx | 11 +++++-- .../PassageDetailsArtifactsMobile.tsx | 12 +++++--- src/renderer/src/control/AltButton.tsx | 29 ++++++++++++++++++- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/PassageDetail/Internalization/AddResource.tsx b/src/renderer/src/components/PassageDetail/Internalization/AddResource.tsx index d1f5085c..075fca6a 100644 --- a/src/renderer/src/components/PassageDetail/Internalization/AddResource.tsx +++ b/src/renderer/src/components/PassageDetail/Internalization/AddResource.tsx @@ -16,10 +16,12 @@ import { usePlanType } from '../../../crud'; interface IProps { action?: (what: string) => void; stopPlayer?: () => void; + buttonDark?: boolean; + buttonElevated?: boolean; // setting for some shadowing } export const AddResource = (props: IProps) => { - const { action, stopPlayer } = props; + const { action, stopPlayer, buttonDark, buttonElevated } = props; const { passage } = usePassageDetailContext(); const { getPassageTypeFromId } = usePassageType(); const [biblebrain, setBiblebrain] = useState(true); @@ -61,7 +63,12 @@ export const AddResource = (props: IProps) => { return (
- + {t.add} {isScripture && ( - handleFindVisible(true)}> - {t.research} + handleFindVisible(true)}> + {t.research} )} {hasPermission && (!offline || offlineOnly) && ( <> - + {hasProjRes && ( diff --git a/src/renderer/src/control/AltButton.tsx b/src/renderer/src/control/AltButton.tsx index f2b7345b..4b943a23 100644 --- a/src/renderer/src/control/AltButton.tsx +++ b/src/renderer/src/control/AltButton.tsx @@ -1,6 +1,17 @@ import { Button, ButtonProps } from '@mui/material'; -export const AltButton = ({ children, ...rest }: ButtonProps) => ( +interface IAltButtonProps extends ButtonProps { + dark?: boolean; + elevated?: boolean; +} + +export const AltButton = ({ + children, + dark, + elevated, + sx, + ...rest +}: IAltButtonProps) => ( + )} + + + ); +}; From 2b16fd247f62d0259361c1fd6e849013d657a7aa Mon Sep 17 00:00:00 2001 From: AbigailLew4444 Date: Fri, 27 Mar 2026 13:22:00 -0700 Subject: [PATCH 20/20] revert mobile calls for testing --- src/renderer/src/routes/PassageDetail.tsx | 31 +++++------------------ 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/src/renderer/src/routes/PassageDetail.tsx b/src/renderer/src/routes/PassageDetail.tsx index 82727f15..da355ff2 100644 --- a/src/renderer/src/routes/PassageDetail.tsx +++ b/src/renderer/src/routes/PassageDetail.tsx @@ -288,7 +288,11 @@ const PassageDetailGrids = () => { - + {isMobile ? ( + + ) : ( + + )} @@ -436,17 +440,6 @@ export const PassageDetail = () => { const [user] = useGlobal('user'); const { setProjectType } = useProjectType(); - const { isMobile } = useMobile(); - const [paneWidth, setPaneWidth] = useState(0); - - useEffect(() => { - if (isMobile) { - setPaneWidth(window.innerWidth); - } else { - setPaneWidth(window.innerWidth - 450); // Start with discussion panel open on desktop - } - }, [isMobile]); - useEffect(() => { const projectId = setUrlContext(prjId ?? ''); if (user && projType === '') { @@ -474,19 +467,7 @@ export const PassageDetail = () => { > - { isMobile ? ( - - {/* */} - - } - noAudioText={''} - /> - ) : ( - - )} + );