diff --git a/.env b/.env index 899dc39..c554530 100644 --- a/.env +++ b/.env @@ -1,4 +1,8 @@ -VITE_API_URL='https://wn.krbl.ru' -VITE_REACT_APP ='https://wn.krbl.ru/' -VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' -VITE_NEED_AUTH='true' \ No newline at end of file +VITE_API_URL='https://wn.st.unprism.ru' +VITE_REACT_APP ='https://wn.st.unprism.ru/' +VITE_KRBL_MEDIA='https://wn.st.unprism.ru/media/' +VITE_NEED_AUTH='true' +# VITE_API_URL='https://wn.krbl.ru' +# VITE_REACT_APP ='https://wn.krbl.ru/' +# VITE_KRBL_MEDIA='https://wn.krbl.ru/media/' +# VITE_NEED_AUTH='true' diff --git a/package.json b/package.json index f2639a6..f3ffd66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "white-nights", "private": true, - "version": "1.0.4", + "version": "1.0.5", "type": "module", "license": "UNLICENSED", "scripts": { diff --git a/src/pages/Route/route-preview/MapDataContext.tsx b/src/pages/Route/route-preview/MapDataContext.tsx index 7b97201..cf7a093 100644 --- a/src/pages/Route/route-preview/MapDataContext.tsx +++ b/src/pages/Route/route-preview/MapDataContext.tsx @@ -36,12 +36,13 @@ const MapDataContext = createContext<{ setMapCenter: (x: number, y: number) => void; setStationOffset: (stationId: number, x: number, y: number) => void; setStationAlign: (stationId: number, align: number) => void; + setStationIconSize: (stationId: number, size: number) => void; setSightCoordinates: ( sightId: number, latitude: number, longitude: number ) => void; - setIconSize: (size: number) => void; + setSightIconSize: (sightId: number, size: number) => void; setFontSize: (size: number) => void; saveChanges: () => void; }>({ @@ -62,8 +63,9 @@ const MapDataContext = createContext<{ setMapCenter: () => {}, setStationOffset: () => {}, setStationAlign: () => {}, + setStationIconSize: () => {}, setSightCoordinates: () => {}, - setIconSize: () => {}, + setSightIconSize: () => {}, setFontSize: () => {}, saveChanges: () => {}, }); @@ -145,16 +147,16 @@ export const MapDataProvider = observer( }, [routeId]); useEffect(() => { - if (originalRouteData) + if (originalRouteData) { setRouteData({ ...originalRouteData, ...routeChanges }); - if (originalSightData) setSightData(originalSightData); - }, [ - originalRouteData, - originalSightData, - routeChanges, - stationChanges, - sightChanges, - ]); + } + }, [originalRouteData, routeChanges]); + + useEffect(() => { + if (originalSightData) { + setSightData(originalSightData); + } + }, [originalSightData]); function setScaleRange(min: number, max: number) { setRouteChanges((prev) => { @@ -168,16 +170,6 @@ export const MapDataProvider = observer( }); } - function setIconSize(size: number) { - const clamped = Math.max(1, Math.min(300, size)); - setRouteChanges((prev) => { - if (prev.icon_size === clamped) { - return prev; - } - return { ...prev, icon_size: clamped }; - }); - } - function setFontSize(size: number) { const clamped = Math.max(1, Math.min(300, size)); setRouteChanges((prev) => { @@ -241,6 +233,11 @@ export const MapDataProvider = observer( ...s, offset_x: station.offset_x, offset_y: station.offset_y, + align: station.align, + icon_size: + typeof station.icon_size === "number" + ? station.icon_size + : s.icon_size, } : s ); @@ -262,6 +259,10 @@ export const MapDataProvider = observer( ...s, latitude: sight.latitude, longitude: sight.longitude, + icon_size: + typeof sight.icon_size === "number" + ? sight.icon_size + : s.icon_size, } : s ) @@ -306,6 +307,7 @@ export const MapDataProvider = observer( offset_x: x, offset_y: y, align: originalStation?.align ?? 1, + icon_size: originalStation?.icon_size, transfers: originalStation?.transfers ?? { bus: "", metro_blue: "", @@ -367,6 +369,7 @@ export const MapDataProvider = observer( align: align, offset_x: originalStation?.offset_x ?? 0, offset_y: originalStation?.offset_y ?? 0, + icon_size: originalStation?.icon_size, transfers: originalStation?.transfers ?? { bus: "", metro_blue: "", @@ -397,6 +400,70 @@ export const MapDataProvider = observer( }); } + function setStationIconSize(stationId: number, size: number) { + const clamped = Math.max(1, Math.min(300, Math.round(size))); + const currentStation = stationData.ru?.find( + (station) => station.id === stationId + ); + if (currentStation?.icon_size === clamped) { + return; + } + + setStationChanges((prev) => { + const existingIndex = prev.findIndex( + (station) => station.station_id === stationId + ); + + if (existingIndex !== -1) { + const next = [...prev]; + next[existingIndex] = { + ...next[existingIndex], + icon_size: clamped, + }; + return next; + } + + const originalStation = originalStationData?.find( + (s) => s.id === stationId + ); + + return [ + ...prev, + { + station_id: stationId, + offset_x: currentStation?.offset_x ?? originalStation?.offset_x ?? 0, + offset_y: currentStation?.offset_y ?? originalStation?.offset_y ?? 0, + align: currentStation?.align ?? originalStation?.align ?? 1, + icon_size: clamped, + transfers: currentStation?.transfers ?? + originalStation?.transfers ?? { + bus: "", + metro_blue: "", + metro_green: "", + metro_orange: "", + metro_purple: "", + metro_red: "", + train: "", + tram: "", + trolleybus: "", + }, + }, + ]; + }); + + setStationData((prev) => { + const updated = { ...prev }; + Object.keys(updated).forEach((lang) => { + updated[lang] = updated[lang].map((station) => + station.id === stationId + ? { ...station, icon_size: clamped } + : station + ); + }); + return updated; + }); + } + function setSightCoordinates( sightId: number, latitude: number, @@ -435,6 +502,7 @@ export const MapDataProvider = observer( sight_id: sightId, latitude, longitude, + icon_size: foundSight.icon_size, }, ]; } @@ -443,6 +511,49 @@ export const MapDataProvider = observer( }); } + function setSightIconSize(sightId: number, size: number) { + const clamped = Math.max(1, Math.min(300, Math.round(size))); + + setSightData((prev) => + prev + ? prev.map((sight) => + sight.id === sightId ? { ...sight, icon_size: clamped } : sight + ) + : prev + ); + + setSightChanges((prev) => { + const existingIndex = prev.findIndex( + (sight) => sight.sight_id === sightId + ); + if (existingIndex !== -1) { + const next = [...prev]; + next[existingIndex] = { + ...next[existingIndex], + icon_size: clamped, + }; + return next; + } + + const foundSight = + sightData?.find((sight) => sight.id === sightId) ?? + originalSightData?.find((sight) => sight.id === sightId); + if (!foundSight) { + return prev; + } + + return [ + ...prev, + { + sight_id: sightId, + latitude: foundSight.latitude, + longitude: foundSight.longitude, + icon_size: clamped, + }, + ]; + }); + } + useEffect(() => {}, [sightChanges]); const value = useMemo( @@ -464,8 +575,9 @@ export const MapDataProvider = observer( saveChanges, setStationOffset, setStationAlign, + setStationIconSize, setSightCoordinates, - setIconSize, + setSightIconSize, setFontSize, }), [ @@ -479,7 +591,8 @@ export const MapDataProvider = observer( isStationLoading, isSightLoading, selectedSight, - setIconSize, + setStationIconSize, + setSightIconSize, setFontSize, ] ); diff --git a/src/pages/Route/route-preview/RightSidebar.tsx b/src/pages/Route/route-preview/RightSidebar.tsx index 565439b..fcdb44c 100644 --- a/src/pages/Route/route-preview/RightSidebar.tsx +++ b/src/pages/Route/route-preview/RightSidebar.tsx @@ -21,7 +21,6 @@ export function RightSidebar() { originalRouteData, setMapRotation, setMapCenter, - setIconSize: updateIconSize, setFontSize: updateFontSize, } = useMapData(); const { rotation, rotateToAngle, scale, setScaleAtCenter } = useTransform(); @@ -34,7 +33,6 @@ export function RightSidebar() { }); const [rotationDegrees, setRotationDegrees] = useState(0); const [isUserEditing, setIsUserEditing] = useState(false); - const [iconSize, setIconSize] = useState(100); const [fontSize, setFontSize] = useState(100); const [isSaving, setIsSaving] = useState(false); @@ -53,7 +51,6 @@ export function RightSidebar() { x: originalRouteData.center_latitude ?? 0, y: originalRouteData.center_longitude ?? 0, }); - setIconSize(originalRouteData.icon_size ?? 100); setFontSize(originalRouteData.font_size ?? 100); } }, [originalRouteData]); @@ -97,15 +94,6 @@ export function RightSidebar() { rotateToAngle((degrees * Math.PI) / 180); } - const handleIconSizeChange = (value: number) => { - if (!Number.isFinite(value)) { - return; - } - const clamped = Math.max(1, Math.min(300, Math.round(value))); - setIconSize(clamped); - updateIconSize(clamped); - }; - const handleFontSizeChange = (value: number) => { if (!Number.isFinite(value)) { return; @@ -115,11 +103,6 @@ export function RightSidebar() { updateFontSize(clamped); }; - useEffect(() => { - const next = routeData?.icon_size ?? originalRouteData?.icon_size ?? 100; - setIconSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev)); - }, [routeData?.icon_size, originalRouteData?.icon_size]); - useEffect(() => { const next = routeData?.font_size ?? originalRouteData?.font_size ?? 100; setFontSize((prev) => (Math.abs(prev - next) > 0.5 ? next : prev)); @@ -307,33 +290,6 @@ export function RightSidebar() { }} /> - { - const value = Number(e.target.value); - if (!isNaN(value)) { - handleIconSizeChange(value); - } - }} - style={{ backgroundColor: "#222", borderRadius: 4 }} - sx={{ - "& .MuiInputLabel-root": { - color: "#fff", - }, - "& .MuiInputBase-input": { - color: "#fff", - }, - }} - inputProps={{ - min: 1, - max: 300, - step: 1, - }} - /> - { }; const computeStationVertices = ( - stations?: Array<{ latitude: number; longitude: number }> + stations?: Array<{ latitude: number; longitude: number }>, ): Float32Array => { if (!stations || stations.length === 0) { return new Float32Array(); @@ -193,7 +193,7 @@ const computeStationVertices = ( const rotateVertices = ( vertices: Float32Array, - angle: number + angle: number, ): Float32Array => { if (!vertices || vertices.length === 0 || Math.abs(angle) < 1e-6) { return vertices; @@ -214,6 +214,8 @@ const rotateVertices = ( }; const DRAG_THRESHOLD_PX = 4; +const ICON_SIZE_MIN = 1; +const ICON_SIZE_MAX = 300; type StationDragState = { stationId: number; @@ -248,9 +250,30 @@ type SightLivePosition = { offsetY: number; }; +type StationIconResizeState = { + stationId: number; + pointerId: number; + initialPointer: Vec2; + baseIconSizePx: number; + initialPercent: number; + lastPercent: number; + captureTarget: HTMLElement | null; +}; + +type SightIconResizeState = { + sightId: number; + pointerId: number; + initialPointer: Vec2; + initialPercent: number; + initialSizePx: number; + baseSizePxAt100: number; + lastPercent: number; + captureTarget: HTMLElement | null; +}; + const generateThickLineGeometry = ( points: Float32Array, - width: number + width: number, ): Float32Array => { if (points.length < 4 || width <= 0) { return new Float32Array(); @@ -322,7 +345,7 @@ const generateThickLineGeometry = ( const computeViewTransform = ( vertices: Float32Array, width: number, - height: number + height: number, ): Transform => { if (vertices.length < 4) { return { @@ -351,7 +374,7 @@ const computeViewTransform = ( const padding = 0.1; const scale = Math.min( (width * (1 - padding)) / worldWidth, - (height * (1 - padding)) / worldHeight + (height * (1 - padding)) / worldHeight, ); const centerX = (minX + maxX) * 0.5; @@ -390,10 +413,13 @@ export const WebGLRouteMapPrototype = observer(() => { routeData, stationData, sightData, + selectedSight, setSelectedSight, setStationOffset, setStationAlign, + setStationIconSize, setSightCoordinates, + setSightIconSize, setMapCenter, } = useMapData(); const { language } = languageStore; @@ -457,6 +483,9 @@ export const WebGLRouteMapPrototype = observer(() => { const stationDragStateRef = useRef(null); const sightDragStateRef = useRef(null); + const stationIconResizeStateRef = useRef(null); + const sightIconResizeStateRef = useRef(null); + const customSightIconBaseScaleRef = useRef(null); const suppressAutoFitRef = useRef(false); const skipNextAutoFitRef = useRef(false); const [liveStationOffsets, setLiveStationOffsets] = useState< @@ -465,6 +494,24 @@ export const WebGLRouteMapPrototype = observer(() => { const [liveSightPositions, setLiveSightPositions] = useState< Map >(new Map()); + const [liveStationIconSizes, setLiveStationIconSizes] = useState< + Map + >(new Map()); + const [liveSightIconSizes, setLiveSightIconSizes] = useState< + Map + >(new Map()); + const [hoveredStationIconId, setHoveredStationIconId] = useState< + number | null + >(null); + const [hoveredSightIconId, setHoveredSightIconId] = useState( + null, + ); + const [resizingStationIconId, setResizingStationIconId] = useState< + number | null + >(null); + const [resizingSightIconId, setResizingSightIconId] = useState( + null, + ); type StationAlignment = "left" | "center" | "right"; const [stationAlignments, setStationAlignments] = useState< Map @@ -495,7 +542,7 @@ export const WebGLRouteMapPrototype = observer(() => { y: clientY - rect.top, }; }, - [] + [], ); const getWorldPosition = useCallback( @@ -508,7 +555,7 @@ export const WebGLRouteMapPrototype = observer(() => { y: (relative.y * dpr - camera.translation.y) / camera.scale, }; }, - [getRelativePointerPosition] + [getRelativePointerPosition], ); const [canvasSize, setCanvasSize] = useState<{ @@ -518,7 +565,7 @@ export const WebGLRouteMapPrototype = observer(() => { const routeVertices = useMemo( () => computeWorldVertices(originalRouteData?.path), - [originalRouteData?.path] + [originalRouteData?.path], ); const stationVertices = useMemo( @@ -527,15 +574,34 @@ export const WebGLRouteMapPrototype = observer(() => { stationData?.ru?.map((station) => ({ latitude: station.latitude, longitude: station.longitude, - })) + })), ), - [stationData?.ru] + [stationData?.ru], ); const rotationAngle = useMemo(() => { const deg = routeData?.rotate ?? originalRouteData?.rotate ?? 0; return (deg * Math.PI) / 180; }, [routeData?.rotate, originalRouteData?.rotate]); + const mediaBaseUrl = import.meta.env.VITE_KRBL_MEDIA; + const mediaToken = localStorage.getItem("token") ?? ""; + + useEffect(() => { + customSightIconBaseScaleRef.current = null; + }, [originalRouteData?.id]); + + useEffect(() => { + const camera = + transformState ?? transformRef.current ?? lastTransformRef.current; + if ( + customSightIconBaseScaleRef.current == null && + camera?.scale && + Number.isFinite(camera.scale) && + camera.scale > 0 + ) { + customSightIconBaseScaleRef.current = camera.scale; + } + }, [transformState]); const computeCenterCoordinates = useCallback( (transform: Transform) => { @@ -570,7 +636,7 @@ export const WebGLRouteMapPrototype = observer(() => { longitude: roundedLon, }; }, - [rotationAngle] + [rotationAngle], ); const cancelScheduledCenterCommit = useCallback(() => { @@ -667,7 +733,7 @@ export const WebGLRouteMapPrototype = observer(() => { const updateTransform = useCallback( ( next: Transform, - options?: { immediate?: boolean; skipClamp?: boolean } + options?: { immediate?: boolean; skipClamp?: boolean }, ) => { const adjusted = options?.skipClamp ? next : clampTransformScale(next); @@ -683,17 +749,17 @@ export const WebGLRouteMapPrototype = observer(() => { } computeCenterCoordinates(adjusted); }, - [clampTransformScale, setSharedScale, computeCenterCoordinates] + [clampTransformScale, setSharedScale, computeCenterCoordinates], ); const rotatedRouteVertices = useMemo( () => rotateVertices(routeVertices, rotationAngle), - [routeVertices, rotationAngle] + [routeVertices, rotationAngle], ); const rotatedStationVertices = useMemo( () => rotateVertices(stationVertices, rotationAngle), - [stationVertices, rotationAngle] + [stationVertices, rotationAngle], ); const resetTransform = useCallback(() => { @@ -743,7 +809,7 @@ export const WebGLRouteMapPrototype = observer(() => { return next; }); }, - [getWorldPosition, setLiveStationOffsets] + [getWorldPosition, setLiveStationOffsets], ); const handleStationPointerUp = useCallback( @@ -767,7 +833,7 @@ export const WebGLRouteMapPrototype = observer(() => { setStationOffset( state.stationId, state.lastOffset.x, - state.lastOffset.y + state.lastOffset.y, ); } @@ -782,7 +848,7 @@ export const WebGLRouteMapPrototype = observer(() => { document.body.style.cursor = ""; } }, - [handleStationPointerMove, setLiveStationOffsets, setStationOffset] + [handleStationPointerMove, setLiveStationOffsets, setStationOffset], ); const handleStationPointerDown = useCallback( @@ -790,7 +856,7 @@ export const WebGLRouteMapPrototype = observer(() => { event: ReactPointerEvent, stationId: number, rotatedBase: Vec2, - currentOffset: Vec2 + currentOffset: Vec2, ) => { event.preventDefault(); event.stopPropagation(); @@ -858,7 +924,7 @@ export const WebGLRouteMapPrototype = observer(() => { transformState, getWorldPosition, setLiveStationOffsets, - ] + ], ); const handleSightPointerMove = useCallback( @@ -871,7 +937,7 @@ export const WebGLRouteMapPrototype = observer(() => { const world = getWorldPosition( event.clientX, event.clientY, - state.camera + state.camera, ); if (!world) return; @@ -916,7 +982,7 @@ export const WebGLRouteMapPrototype = observer(() => { return next; }); }, - [getWorldPosition, setLiveSightPositions] + [getWorldPosition, setLiveSightPositions], ); const handleSightPointerUp = useCallback( @@ -940,7 +1006,7 @@ export const WebGLRouteMapPrototype = observer(() => { setSightCoordinates( state.sight.id, state.lastCoordinates.latitude, - state.lastCoordinates.longitude + state.lastCoordinates.longitude, ); } else { setSelectedSight(state.sight); @@ -951,7 +1017,7 @@ export const WebGLRouteMapPrototype = observer(() => { sightDragStateRef.current = null; }, - [handleSightPointerMove, setSelectedSight, setSightCoordinates] + [handleSightPointerMove, setSelectedSight, setSightCoordinates], ); const handleSightPointerDown = useCallback( @@ -960,7 +1026,7 @@ export const WebGLRouteMapPrototype = observer(() => { sight: SightData, offset: Vec2, rotatedBase: Vec2, - currentCoords: { latitude: number; longitude: number } + currentCoords: { latitude: number; longitude: number }, ) => { event.preventDefault(); event.stopPropagation(); @@ -974,7 +1040,7 @@ export const WebGLRouteMapPrototype = observer(() => { const pointerWorld = getWorldPosition( event.clientX, event.clientY, - camera + camera, ); const labelWorldX = rotatedBase.x + offset.x; const labelWorldY = rotatedBase.y + offset.y; @@ -1018,7 +1084,221 @@ export const WebGLRouteMapPrototype = observer(() => { rotationAngle, transformState, getWorldPosition, - ] + ], + ); + + const handleStationIconResizeMove = useCallback((event: PointerEvent) => { + const state = stationIconResizeStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + event.preventDefault(); + + const dx = event.clientX - state.initialPointer.x; + const dy = event.clientY - state.initialPointer.y; + const deltaPx = (dx + dy) * 0.5; + const initialSizePx = state.baseIconSizePx * (state.initialPercent / 100); + const nextSizePx = Math.max(1, initialSizePx + deltaPx); + const nextPercent = clamp( + Math.round((nextSizePx / Math.max(1, state.baseIconSizePx)) * 100), + ICON_SIZE_MIN, + ICON_SIZE_MAX, + ); + + if (nextPercent === state.lastPercent) { + return; + } + + state.lastPercent = nextPercent; + setLiveStationIconSizes((prev) => { + const next = new Map(prev); + next.set(state.stationId, nextPercent); + return next; + }); + }, []); + + const handleStationIconResizeUp = useCallback( + (event: PointerEvent) => { + const state = stationIconResizeStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + if (state.captureTarget?.releasePointerCapture) { + state.captureTarget.releasePointerCapture(event.pointerId); + } + + window.removeEventListener("pointermove", handleStationIconResizeMove); + window.removeEventListener("pointerup", handleStationIconResizeUp); + window.removeEventListener("pointercancel", handleStationIconResizeUp); + + setLiveStationIconSizes((prev) => { + if (!prev.has(state.stationId)) return prev; + const next = new Map(prev); + next.delete(state.stationId); + return next; + }); + + if (state.lastPercent !== state.initialPercent) { + setStationIconSize(state.stationId, state.lastPercent); + } + + stationIconResizeStateRef.current = null; + setResizingStationIconId(null); + if (typeof document !== "undefined") { + document.body.style.cursor = ""; + } + }, + [handleStationIconResizeMove, setStationIconSize], + ); + + const handleStationIconResizeDown = useCallback( + ( + event: ReactPointerEvent, + stationId: number, + currentPercent: number, + baseIconSizePx: number, + ) => { + event.preventDefault(); + event.stopPropagation(); + + const captureTarget = event.currentTarget; + if (captureTarget.setPointerCapture) { + captureTarget.setPointerCapture(event.pointerId); + } + + stationIconResizeStateRef.current = { + stationId, + pointerId: event.pointerId, + initialPointer: { x: event.clientX, y: event.clientY }, + baseIconSizePx: Math.max(1, baseIconSizePx), + initialPercent: currentPercent, + lastPercent: currentPercent, + captureTarget, + }; + setResizingStationIconId(stationId); + + setLiveStationIconSizes((prev) => { + const next = new Map(prev); + next.set(stationId, currentPercent); + return next; + }); + + if (typeof document !== "undefined") { + document.body.style.cursor = "nwse-resize"; + } + window.addEventListener("pointermove", handleStationIconResizeMove); + window.addEventListener("pointerup", handleStationIconResizeUp); + window.addEventListener("pointercancel", handleStationIconResizeUp); + }, + [handleStationIconResizeMove, handleStationIconResizeUp], + ); + + const handleSightIconResizeMove = useCallback((event: PointerEvent) => { + const state = sightIconResizeStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + event.preventDefault(); + + const dx = event.clientX - state.initialPointer.x; + const dy = event.clientY - state.initialPointer.y; + const deltaPx = (dx + dy) * 0.5; + const nextSizePx = Math.max(1, state.initialSizePx + deltaPx); + const nextPercent = clamp( + Math.round((nextSizePx / state.baseSizePxAt100) * 100), + ICON_SIZE_MIN, + ICON_SIZE_MAX, + ); + + if (nextPercent === state.lastPercent) { + return; + } + + state.lastPercent = nextPercent; + setLiveSightIconSizes((prev) => { + const next = new Map(prev); + next.set(state.sightId, nextPercent); + return next; + }); + }, []); + + const handleSightIconResizeUp = useCallback( + (event: PointerEvent) => { + const state = sightIconResizeStateRef.current; + if (!state || event.pointerId !== state.pointerId) return; + + if (state.captureTarget?.releasePointerCapture) { + state.captureTarget.releasePointerCapture(event.pointerId); + } + + window.removeEventListener("pointermove", handleSightIconResizeMove); + window.removeEventListener("pointerup", handleSightIconResizeUp); + window.removeEventListener("pointercancel", handleSightIconResizeUp); + + setLiveSightIconSizes((prev) => { + if (!prev.has(state.sightId)) return prev; + const next = new Map(prev); + next.delete(state.sightId); + return next; + }); + + if (state.lastPercent !== state.initialPercent) { + setSightIconSize(state.sightId, state.lastPercent); + } + + sightIconResizeStateRef.current = null; + setResizingSightIconId(null); + if (typeof document !== "undefined") { + document.body.style.cursor = ""; + } + }, + [handleSightIconResizeMove, setSightIconSize], + ); + + const handleSightIconResizeDown = useCallback( + ( + event: ReactPointerEvent, + sightId: number, + currentPercent: number, + initialSizePx: number, + ) => { + event.preventDefault(); + event.stopPropagation(); + + const captureTarget = event.currentTarget; + if (captureTarget.setPointerCapture) { + captureTarget.setPointerCapture(event.pointerId); + } + + const safePercent = Math.max(ICON_SIZE_MIN, currentPercent); + const baseSizePxAt100 = Math.max( + 1, + initialSizePx / (safePercent / 100), + ); + + sightIconResizeStateRef.current = { + sightId, + pointerId: event.pointerId, + initialPointer: { x: event.clientX, y: event.clientY }, + initialPercent: safePercent, + initialSizePx, + baseSizePxAt100, + lastPercent: safePercent, + captureTarget, + }; + setResizingSightIconId(sightId); + + setLiveSightIconSizes((prev) => { + const next = new Map(prev); + next.set(sightId, safePercent); + return next; + }); + + if (typeof document !== "undefined") { + document.body.style.cursor = "nwse-resize"; + } + window.addEventListener("pointermove", handleSightIconResizeMove); + window.addEventListener("pointerup", handleSightIconResizeUp); + window.addEventListener("pointercancel", handleSightIconResizeUp); + }, + [handleSightIconResizeMove, handleSightIconResizeUp], ); useEffect(() => { @@ -1029,12 +1309,22 @@ export const WebGLRouteMapPrototype = observer(() => { window.removeEventListener("pointermove", handleSightPointerMove); window.removeEventListener("pointerup", handleSightPointerUp); window.removeEventListener("pointercancel", handleSightPointerUp); + window.removeEventListener("pointermove", handleStationIconResizeMove); + window.removeEventListener("pointerup", handleStationIconResizeUp); + window.removeEventListener("pointercancel", handleStationIconResizeUp); + window.removeEventListener("pointermove", handleSightIconResizeMove); + window.removeEventListener("pointerup", handleSightIconResizeUp); + window.removeEventListener("pointercancel", handleSightIconResizeUp); }; }, [ handleStationPointerMove, handleStationPointerUp, handleSightPointerMove, handleSightPointerUp, + handleStationIconResizeMove, + handleStationIconResizeUp, + handleSightIconResizeMove, + handleSightIconResizeUp, ]); const ensureContext = useCallback(() => { @@ -1056,12 +1346,12 @@ export const WebGLRouteMapPrototype = observer(() => { const lineProgram = createProgram( gl, lineVertexSource, - lineFragmentSource + lineFragmentSource, ); const pointProgram = createProgram( gl, pointVertexSource, - pointFragmentSource + pointFragmentSource, ); lineProgramRef.current = lineProgram; @@ -1069,7 +1359,7 @@ export const WebGLRouteMapPrototype = observer(() => { lineBufferRef.current = gl.createBuffer(); pointBufferRef.current = gl.createBuffer(); - } catch (error) { + } catch { // console.error("Failed to initialize WebGL", error); } }, []); @@ -1139,7 +1429,7 @@ export const WebGLRouteMapPrototype = observer(() => { backgroundColor[0], backgroundColor[1], backgroundColor[2], - 1 + 1, ); gl.clear(gl.COLOR_BUFFER_BIT); @@ -1172,7 +1462,7 @@ export const WebGLRouteMapPrototype = observer(() => { transform = computeViewTransform( fallbackVertices, canvas.width, - canvas.height + canvas.height, ); if (!hasRouteScaleLimits) { const baseScale = Math.max(0.1, transform.scale || 1); @@ -1193,7 +1483,7 @@ export const WebGLRouteMapPrototype = observer(() => { ) { const local = coordinatesToLocal( centerLat as number, - centerLon as number + centerLon as number, ); const baseX = local.x * UP_SCALE; const baseY = local.y * UP_SCALE; @@ -1253,7 +1543,7 @@ export const WebGLRouteMapPrototype = observer(() => { const lineWidth = (desiredRouteWidthCss * dpr) / scale; const thickVertices = generateThickLineGeometry( rotatedRouteVertices, - lineWidth + lineWidth, ); if (thickVertices.length === 0) { gl.bufferData(gl.ARRAY_BUFFER, rotatedRouteVertices, gl.STATIC_DRAW); @@ -1268,21 +1558,21 @@ export const WebGLRouteMapPrototype = observer(() => { gl.FLOAT, false, 0, - 0 + 0, ); if (lineProgram.uniformLocations.u_resolution) { gl.uniform2f( lineProgram.uniformLocations.u_resolution, canvas.width, - canvas.height + canvas.height, ); } if (lineProgram.uniformLocations.u_translation) { gl.uniform2f( lineProgram.uniformLocations.u_translation, translation.x, - translation.y + translation.y, ); } if (lineProgram.uniformLocations.u_scale) { @@ -1294,7 +1584,7 @@ export const WebGLRouteMapPrototype = observer(() => { pathColor[0], pathColor[1], pathColor[2], - 1 + 1, ); } @@ -1318,21 +1608,21 @@ export const WebGLRouteMapPrototype = observer(() => { gl.FLOAT, false, 0, - 0 + 0, ); if (pointProgram.uniformLocations.u_resolution) { gl.uniform2f( pointProgram.uniformLocations.u_resolution, canvas.width, - canvas.height + canvas.height, ); } if (pointProgram.uniformLocations.u_translation) { gl.uniform2f( pointProgram.uniformLocations.u_translation, translation.x, - translation.y + translation.y, ); } if (pointProgram.uniformLocations.u_scale) { @@ -1342,7 +1632,7 @@ export const WebGLRouteMapPrototype = observer(() => { if (pointProgram.uniformLocations.u_pointSize) { gl.uniform1f( pointProgram.uniformLocations.u_pointSize, - pointOuterSizePx + pointOuterSizePx, ); } if (pointProgram.uniformLocations.u_color) { @@ -1351,14 +1641,14 @@ export const WebGLRouteMapPrototype = observer(() => { backgroundColor[0], backgroundColor[1], backgroundColor[2], - 1 + 1, ); } gl.drawArrays(gl.POINTS, 0, rotatedStationVertices.length / 2); if (pointProgram.uniformLocations.u_pointSize) { gl.uniform1f( pointProgram.uniformLocations.u_pointSize, - pointInnerSizePx + pointInnerSizePx, ); } if (pointProgram.uniformLocations.u_color) { @@ -1367,7 +1657,7 @@ export const WebGLRouteMapPrototype = observer(() => { pathColor[0], pathColor[1], pathColor[2], - 1 + 1, ); } gl.drawArrays(gl.POINTS, 0, rotatedStationVertices.length / 2); @@ -1476,7 +1766,7 @@ export const WebGLRouteMapPrototype = observer(() => { if (Number.isFinite(centerLat) && Number.isFinite(centerLon)) { const local = coordinatesToLocal( centerLat as number, - centerLon as number + centerLon as number, ); const baseX = local.x * UP_SCALE; const baseY = local.y * UP_SCALE; @@ -1581,7 +1871,7 @@ export const WebGLRouteMapPrototype = observer(() => { const currentTranslation = transform.translation; const distance = Math.hypot( targetTranslation.x - currentTranslation.x, - targetTranslation.y - currentTranslation.y + targetTranslation.y - currentTranslation.y, ); if (distance < 0.5) { @@ -1600,7 +1890,7 @@ export const WebGLRouteMapPrototype = observer(() => { drawSceneRef.current(); pendingCenterRef.current = null; }, - [rotationAngle, setTransformState, transformState] + [rotationAngle, setTransformState, transformState], ); useEffect(() => { @@ -1747,7 +2037,7 @@ export const WebGLRouteMapPrototype = observer(() => { const clampedScale = clamp( unclampedScale, scaleLimitsRef.current.min, - scaleLimitsRef.current.max + scaleLimitsRef.current.max, ); updateTransform( @@ -1758,7 +2048,7 @@ export const WebGLRouteMapPrototype = observer(() => { y: midpoint.y - worldMidpoint.y * clampedScale, }, }, - { immediate: true } + { immediate: true }, ); drawSceneRef.current(); } @@ -1798,7 +2088,7 @@ export const WebGLRouteMapPrototype = observer(() => { const clampedScale = clamp( unclampedScale, scaleLimitsRef.current.min, - scaleLimitsRef.current.max + scaleLimitsRef.current.max, ); if (clampedScale === transform.scale) { @@ -1818,7 +2108,7 @@ export const WebGLRouteMapPrototype = observer(() => { y: position.y - worldPoint.y * clampedScale, }, }, - { immediate: true } + { immediate: true }, ); drawSceneRef.current(); scheduleCenterCommit(); @@ -1880,7 +2170,7 @@ export const WebGLRouteMapPrototype = observer(() => { const local = coordinatesToLocal( station.latitude, - station.longitude + station.longitude, ); const baseX = local.x * UP_SCALE; const baseY = local.y * UP_SCALE; @@ -1949,8 +2239,8 @@ export const WebGLRouteMapPrototype = observer(() => { backendAlign === 1 ? "left" : backendAlign === 3 - ? "right" - : "center"; + ? "right" + : "center"; const alignment: StationAlignment = stationAlignments.get(station.id) ?? alignmentFromData; @@ -1958,17 +2248,33 @@ export const WebGLRouteMapPrototype = observer(() => { alignment === "left" ? { left: 0, transform: "none" } : alignment === "right" - ? { right: 0, transform: "none" } - : { left: "50%", transform: "translateX(-50%)" }; + ? { right: 0, transform: "none" } + : { left: "50%", transform: "translateX(-50%)" }; - let isMediaIdEmptyResult = isMediaIdEmpty(station.icon); + const isMediaIdEmptyResult = isMediaIdEmpty(station.icon); const iconUrl = isMediaIdEmptyResult ? null : `${import.meta.env.VITE_KRBL_MEDIA}${ station.icon }/download?token=${localStorage.getItem("token") ?? ""}`; - const iconSizePx = Math.round(primaryFontSize * 1.2); + const stationIconSizePercent = + liveStationIconSizes.get(station.id) ?? + (typeof station.icon_size === "number" && + Number.isFinite(station.icon_size) + ? station.icon_size + : 100); + const baseStationIconSizePx = primaryFontSize * 1.2; + const iconSizePx = Math.max( + 1, + Math.round( + baseStationIconSizePx * + clamp(stationIconSizePercent / 100, 0.1, 10), + ), + ); + const showStationResizeUi = + hoveredStationIconId === station.id || + resizingStationIconId === station.id; const secondaryLineHeight = 1.2; const secondaryHeight = showSecondary @@ -1984,7 +2290,7 @@ export const WebGLRouteMapPrototype = observer(() => { onMouseEnter={() => setHoveredStationId(station.id)} onMouseLeave={() => setHoveredStationId((prev) => - prev === station.id ? null : prev + prev === station.id ? null : prev, ) } onPointerDown={(event) => @@ -1995,7 +2301,7 @@ export const WebGLRouteMapPrototype = observer(() => { x: rotatedX, y: rotatedY, }, - { x: offsetX, y: offsetY } + { x: offsetX, y: offsetY }, ) } style={{ @@ -2014,14 +2320,14 @@ export const WebGLRouteMapPrototype = observer(() => { >
{ display: "inline-flex", alignItems: "center", gap: iconUrl ? 6 : 0, - pointerEvents: "none", + pointerEvents: "auto", }} > {iconUrl ? ( - + > +
+ setHoveredStationIconId(station.id) + } + onPointerLeave={() => + setHoveredStationIconId((prev) => + prev === station.id ? null : prev, + ) + } + style={{ + position: "absolute", + inset: -8, + pointerEvents: "auto", + zIndex: 0, + }} + /> + + {showStationResizeUi ? ( +
+ ) : null} + {showStationResizeUi ? ( +
+ setHoveredStationIconId(station.id) + } + onPointerDown={(event) => + handleStationIconResizeDown( + event, + station.id, + stationIconSizePercent, + baseStationIconSizePx, + ) + } + title="Потяните для изменения размера иконки" + style={{ + position: "absolute", + right: -5, + bottom: -5, + width: 10, + height: 10, + border: "1.5px solid #0D99FF", + borderRadius: 1, + cursor: "nwse-resize", + pointerEvents: "auto", + zIndex: -1, + boxSizing: "border-box", + }} + /> + ) : null} +
) : null}
{ const next = new Map(prev); next.set( station.id, - btn.align as StationAlignment + btn.align as StationAlignment, ); return next; }); @@ -2174,8 +2546,8 @@ export const WebGLRouteMapPrototype = observer(() => { const rotatedX = baseX * cos - baseY * sin; const rotatedY = baseX * sin + baseY * cos; - const rawOffsetX = (sight as any)?.offset_x ?? 0; - const rawOffsetY = (sight as any)?.offset_y ?? 0; + const rawOffsetX = sight.offset_x ?? 0; + const rawOffsetY = sight.offset_y ?? 0; const DEFAULT_LABEL_OFFSET_X = 25; const DEFAULT_LABEL_OFFSET_Y = 0; @@ -2199,9 +2571,31 @@ export const WebGLRouteMapPrototype = observer(() => { const dpr = Math.max(1, window.devicePixelRatio || 1); const cssX = labelX / dpr; const cssY = labelY / dpr; - const iconSizePercent = - routeData?.icon_size ?? originalRouteData?.icon_size ?? 100; - const iconSize = 30 * (iconSizePercent / 100); + const shouldUseCustomSightIcon = + sight.is_default_icon === false && + !isMediaIdEmpty(sight.icon); + const sightIconUrl = shouldUseCustomSightIcon + ? `${mediaBaseUrl}${sight.icon}/download?token=${mediaToken}` + : SIGHT_ICON_URL; + const customSightIconScaleFactor = shouldUseCustomSightIcon + ? camera.scale / + Math.max(customSightIconBaseScaleRef.current ?? 1, 1e-6) + : 1; + const sightIconSizePercent = + liveSightIconSizes.get(sight.id) ?? + (typeof sight.icon_size === "number" && + Number.isFinite(sight.icon_size) + ? sight.icon_size + : (routeData?.icon_size ?? + originalRouteData?.icon_size ?? + 100)); + const iconSize = + 30 * + clamp(sightIconSizePercent / 100, 0.1, 10) * + customSightIconScaleFactor; + const showSightResizeUi = + hoveredSightIconId === sight.id || + resizingSightIconId === sight.id; const iconLeft = cssX - iconSize; const iconTop = cssY - iconSize; const labelHeight = 24; @@ -2220,7 +2614,7 @@ export const WebGLRouteMapPrototype = observer(() => { y: offsetY, }, { x: rotatedX, y: rotatedY }, - { latitude, longitude } + { latitude, longitude }, ) } style={{ @@ -2237,12 +2631,89 @@ export const WebGLRouteMapPrototype = observer(() => { touchAction: "none", }} > - +
+
{ + setHoveredSightIconId(sight.id); + setSelectedSight(sight); + }} + onPointerLeave={() => { + setHoveredSightIconId((prev) => + prev === sight.id ? null : prev, + ); + if ( + resizingSightIconId !== sight.id && + sightDragStateRef.current?.sight.id !== sight.id && + selectedSight?.id === sight.id + ) { + setSelectedSight(undefined); + } + }} + style={{ + position: "absolute", + inset: -8, + pointerEvents: "auto", + zIndex: 0, + }} + /> + + {showSightResizeUi ? ( +
+ ) : null} + {showSightResizeUi ? ( +
setHoveredSightIconId(sight.id)} + onPointerDown={(event) => + handleSightIconResizeDown( + event, + sight.id, + sightIconSizePercent, + iconSize, + ) + } + title="Потяните для изменения размера иконки" + style={{ + position: "absolute", + right: -10, + bottom: -10, + width: 10, + height: 10, + border: "1.5px solid #0D99FF", + backgroundColor: "#FFFFFF", + borderRadius: 1, + cursor: "nwse-resize", + pointerEvents: "auto", + zIndex: 0, + boxSizing: "border-box", + }} + /> + ) : null} +
void; hardcodeType?: | "thumbnail" + | "icon" + | "alt_icon" | "watermark_lu" | "watermark_rd" | "image" diff --git a/src/shared/store/CreateSightStore/index.tsx b/src/shared/store/CreateSightStore/index.tsx index 762cbe4..646c0fc 100644 --- a/src/shared/store/CreateSightStore/index.tsx +++ b/src/shared/store/CreateSightStore/index.tsx @@ -30,7 +30,10 @@ type SightCommonInfo = { city: string; latitude: number; longitude: number; + is_default_icon: boolean; thumbnail: string | null; + icon: string | null; + alt_icon: string | null; watermark_lu: string | null; watermark_rd: string | null; left_article: number; @@ -47,7 +50,10 @@ const initialSightState: SightBaseInfo = { city: "", latitude: 0, longitude: 0, + is_default_icon: false, thumbnail: null, + icon: null, + alt_icon: null, watermark_lu: null, watermark_rd: null, left_article: 0, @@ -486,9 +492,12 @@ class CreateSightStore { city: this.sight.city, latitude: this.sight.latitude, longitude: this.sight.longitude, + is_default_icon: this.sight.is_default_icon, name: this.sight[primaryLanguage].name, address: this.sight[primaryLanguage].address, thumbnail: this.sight.thumbnail, + icon: this.sight.icon, + alt_icon: this.sight.alt_icon, watermark_lu: this.sight.watermark_lu, watermark_rd: this.sight.watermark_rd, left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId, @@ -511,9 +520,12 @@ class CreateSightStore { city: this.sight.city, latitude: this.sight.latitude, longitude: this.sight.longitude, + is_default_icon: this.sight.is_default_icon, name: this.sight[lang].name, address: this.sight[lang].address, thumbnail: this.sight.thumbnail, + icon: this.sight.icon, + alt_icon: this.sight.alt_icon, watermark_lu: this.sight.watermark_lu, watermark_rd: this.sight.watermark_rd, left_article: finalLeftArticleId === 0 ? null : finalLeftArticleId, diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx index 15aa5d2..d46e59b 100644 --- a/src/shared/store/EditSightStore/index.tsx +++ b/src/shared/store/EditSightStore/index.tsx @@ -30,7 +30,10 @@ export type SightCommonInfo = { city: string; latitude: number; longitude: number; + is_default_icon: boolean; thumbnail: string | null; + icon: string | null; + alt_icon: string | null; watermark_lu: string | null; watermark_rd: string | null; left_article: number; @@ -51,7 +54,10 @@ class EditSightStore { city: "", latitude: 0, longitude: 0, + is_default_icon: false, thumbnail: null, + icon: null, + alt_icon: null, watermark_lu: null, watermark_rd: null, left_article: 0, @@ -184,7 +190,10 @@ class EditSightStore { city: "", latitude: 0, longitude: 0, + is_default_icon: false, thumbnail: null, + icon: null, + alt_icon: null, watermark_lu: null, watermark_rd: null, left_article: 0, diff --git a/src/shared/store/SightsStore/index.tsx b/src/shared/store/SightsStore/index.tsx index 4198c1f..c153b6c 100644 --- a/src/shared/store/SightsStore/index.tsx +++ b/src/shared/store/SightsStore/index.tsx @@ -23,7 +23,10 @@ export type Sight = { address: string; latitude: number; longitude: number; + is_default_icon: boolean; thumbnail: string | null; + icon: string | null; + alt_icon: string | null; watermark_lu: string | null; watermark_rd: string | null; left_article: number; @@ -174,7 +177,10 @@ class SightsStore { city_id: this.sight?.city_id, latitude: this.sight?.latitude, longitude: this.sight?.longitude, + is_default_icon: this.sight?.is_default_icon, thumbnail: this.sight?.thumbnail, + icon: this.sight?.icon, + alt_icon: this.sight?.alt_icon, watermark_lu: this.sight?.watermark_lu, watermark_rd: this.sight?.watermark_rd, left_article: this.sight?.left_article, diff --git a/src/shared/store/SnapshotStore/index.ts b/src/shared/store/SnapshotStore/index.ts index 5fa022b..afce832 100644 --- a/src/shared/store/SnapshotStore/index.ts +++ b/src/shared/store/SnapshotStore/index.ts @@ -97,7 +97,10 @@ class SnapshotStore { city: "", latitude: 0, longitude: 0, + is_default_icon: false, thumbnail: null, + icon: null, + alt_icon: null, watermark_lu: null, watermark_rd: null, left_article: 0, @@ -134,7 +137,10 @@ class SnapshotStore { city: "", latitude: 0, longitude: 0, + is_default_icon: false, thumbnail: null, + icon: null, + alt_icon: null, watermark_lu: null, watermark_rd: null, left_article: 0, diff --git a/src/widgets/ImageUploadCard/index.tsx b/src/widgets/ImageUploadCard/index.tsx index e7df808..6c48cad 100644 --- a/src/widgets/ImageUploadCard/index.tsx +++ b/src/widgets/ImageUploadCard/index.tsx @@ -5,7 +5,13 @@ import { editSightStore } from "@shared"; import { toast } from "react-toastify"; interface ImageUploadCardProps { title: string; - imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd" | "image"; + imageKey?: + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "image"; imageUrl: string | null | undefined; onImageClick: () => void; onDeleteImageClick: () => void; diff --git a/src/widgets/SightTabs/CreateInformationTab/index.tsx b/src/widgets/SightTabs/CreateInformationTab/index.tsx index 6c8499a..9b53458 100644 --- a/src/widgets/SightTabs/CreateInformationTab/index.tsx +++ b/src/widgets/SightTabs/CreateInformationTab/index.tsx @@ -9,6 +9,8 @@ import { DialogTitle, DialogContent, DialogActions, + Checkbox, + FormControlLabel, } from "@mui/material"; import { BackButton, @@ -54,12 +56,24 @@ export const CreateInformationTab = observer( const [menuAnchorEl, setMenuAnchorEl] = useState(null); const [activeMenuType, setActiveMenuType] = useState< - "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "video_preview" + | null >(null); const [isAddMediaOpen, setIsAddMediaOpen] = useState(false); const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); const [hardcodeType, setHardcodeType] = useState< - "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "video_preview" + | null >(null); // НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА @@ -129,7 +143,13 @@ export const CreateInformationTab = observer( media_name?: string; media_type: number; }, - type: "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" + type: + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "video_preview" ) => { handleChange({ [type]: media.id, @@ -284,6 +304,20 @@ export const CreateInformationTab = observer( variant="outlined" placeholder="Введите координаты в формате: широта долгота" /> + + + handleChange({ + is_default_icon: e.target.checked, + }) + } + /> + } + label="Использовать иконку по умолчанию" + /> + {!sight.is_default_icon && ( + <> + { + setIsPreviewMediaOpen(true); + setMediaId(sight.icon ?? ""); + }} + onDeleteImageClick={() => { + handleChange({ + icon: null, + }); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("icon"); + setIsAddMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("icon"); + setHardcodeType("icon"); + }} + setHardcodeType={() => { + setHardcodeType("icon"); + }} + /> + + { + setIsPreviewMediaOpen(true); + setMediaId(sight.alt_icon ?? ""); + }} + onDeleteImageClick={() => { + handleChange({ + alt_icon: null, + }); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("alt_icon"); + setIsAddMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("alt_icon"); + setHardcodeType("alt_icon"); + }} + setHardcodeType={() => { + setHardcodeType("alt_icon"); + }} + /> + + )} + (null); const [activeMenuType, setActiveMenuType] = useState< - "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "video_preview" + | null >(null); const [isAddMediaOpen, setIsAddMediaOpen] = useState(false); const [isVideoPreviewOpen, setIsVideoPreviewOpen] = useState(false); const [hardcodeType, setHardcodeType] = useState< - "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "video_preview" + | null >(null); const canReadCities = authStore.canRead("cities"); @@ -121,7 +135,13 @@ export const InformationTab = observer( media_name?: string; media_type: number; }, - type: "thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" + type: + | "thumbnail" + | "icon" + | "alt_icon" + | "watermark_lu" + | "watermark_rd" + | "video_preview" ) => { handleChange( language as Language, @@ -295,6 +315,24 @@ export const InformationTab = observer( variant="outlined" placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)" /> + + + handleChange( + language as Language, + { + is_default_icon: e.target.checked, + }, + true + ) + } + /> + } + label="Использовать иконку по умолчанию" + /> @@ -319,11 +357,14 @@ export const InformationTab = observer( > + {!sight.common.is_default_icon && ( + <> + { + setIsPreviewMediaOpen(true); + setMediaId(sight.common.icon ?? ""); + }} + onDeleteImageClick={() => { + handleChange( + language as Language, + { + icon: null, + }, + true + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("icon"); + setIsAddMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("icon"); + setHardcodeType("icon"); + }} + setHardcodeType={() => { + setHardcodeType("icon"); + }} + /> + { + setIsPreviewMediaOpen(true); + setMediaId(sight.common.alt_icon ?? ""); + }} + onDeleteImageClick={() => { + handleChange( + language as Language, + { + alt_icon: null, + }, + true + ); + setActiveMenuType(null); + }} + onSelectFileClick={() => { + setActiveMenuType("alt_icon"); + setIsAddMediaOpen(true); + }} + setUploadMediaOpen={() => { + setIsUploadMediaOpen(true); + setActiveMenuType("alt_icon"); + setHardcodeType("alt_icon"); + }} + setHardcodeType={() => { + setHardcodeType("alt_icon"); + }} + /> + + )} = ({ }; const handleFileInputChange = async ( - event: React.ChangeEvent + event: React.ChangeEvent, ) => { const file = event.target.files?.[0]; if (file) { @@ -87,7 +87,7 @@ export const VideoPreviewCard: React.FC = ({ gap: 1, flex: 1, minWidth: 150, - width: "min-content", + width: "100%", mx: "auto", }} className={className}