fix: lightbox, breadcrumbs, navbar, uninstall

This commit is contained in:
David Ralph
2026-01-03 17:52:10 +00:00
parent 7c2e17b44d
commit 7807462c7b
6 changed files with 91 additions and 163 deletions

View File

@@ -49,8 +49,8 @@ function ModalTopBar({
// Get the last breadcrumb item (the item name) // Get the last breadcrumb item (the item name)
const lastCrumb = iframeBreadcrumbs[iframeBreadcrumbs.length - 1]; const lastCrumb = iframeBreadcrumbs[iframeBreadcrumbs.length - 1];
// Add current section if available // Add current section if available and different from the last crumb
if (currentSection) { if (currentSection && currentSection !== lastCrumb.label) {
breadcrumbPath.push({ breadcrumbPath.push({
label: currentSection, label: currentSection,
onClick: () => onBack(), // Clickable to go back onClick: () => onBack(), // Clickable to go back

View File

@@ -1,12 +1,22 @@
@use 'scss/variables' as *; @use 'scss/variables' as *;
.modalTopBar { .modalTopBar {
position: sticky;
top: 0;
z-index: 10;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 1.5rem 1.5rem; padding: 1.5rem 1.5rem;
// width: 100%; // width: 100%;
@include themed {
background: t($modal);
}
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
.topBarLeft { .topBarLeft {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -6,7 +6,7 @@ export const OPENSTREETMAP_URL = 'https://www.openstreetmap.org';
// Mue URLs // Mue URLs
export const WEBSITE_URL = 'https://muetab.com'; export const WEBSITE_URL = 'https://muetab.com';
export const MARKETPLACE_URL = 'https://muetab.com/marketplace'; export const MARKETPLACE_URL = 'http://localhost:3000/marketplace';
export const PRIVACY_URL = 'https://muetab.com/privacy'; export const PRIVACY_URL = 'https://muetab.com/privacy';
export const TRANSLATIONS_URL = 'https://muetab.com/docs/translations'; export const TRANSLATIONS_URL = 'https://muetab.com/docs/translations';
export const WEBLATE_URL = 'https://hosted.weblate.org/projects/mue/mue-tab/'; export const WEBLATE_URL = 'https://hosted.weblate.org/projects/mue/mue-tab/';

View File

@@ -227,12 +227,6 @@ class About extends PureComponent {
icon={<BiDonateHeart />} icon={<BiDonateHeart />}
label={variables.getMessage('modals.main.settings.sections.about.support_donate')} label={variables.getMessage('modals.main.settings.sections.about.support_donate')}
/> />
<Button
type="linkIconButton"
href={'https://github.com/sponsors/' + variables.constants.ORG_NAME}
icon={<SiGithubsponsors />}
tooltipTitle="Github Sponsors"
/>
</div> </div>
</div> </div>

View File

@@ -1,169 +1,60 @@
import variables from 'config/variables'; import variables from 'config/variables';
import { useState, useEffect, useRef } from 'react'; import { useState } from 'react';
import { MdOutlineWifiOff } from 'react-icons/md'; import { MdOutlineWifiOff } from 'react-icons/md';
import Modal from 'react-modal';
import Lightbox from '../../marketplace/components/Elements/Lightbox/Lightbox';
const Changelog = () => { const Changelog = () => {
const [title, setTitle] = useState(null); const [isLoading, setIsLoading] = useState(true);
const [content, setContent] = useState(null);
const [date, setDate] = useState(null);
const [image, setImage] = useState(null);
const [error, setError] = useState(false);
const [showLightbox, setShowLightbox] = useState(false);
const [lightboxImg, setLightboxImg] = useState(null);
const offlineMode = localStorage.getItem('offlineMode') === 'true'; const offlineMode = localStorage.getItem('offlineMode') === 'true';
const controllerRef = useRef(new AbortController()); const isOffline = navigator.onLine === false || offlineMode;
const changelog = useRef();
const parseMarkdown = (text) => { const handleLoad = () => {
if (typeof text !== 'string') { setIsLoading(false);
throw new Error('Input must be a string');
}
// Replace markdown syntax
text = text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/^## (.*$)/gm, '<span class="title">$1</span>')
.replace(
/((http|https):\/\/[^\s]+)/g,
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
)
// resolve @ to github user link
.replace(
/@([a-zA-Z0-9-_]+)/g,
'<a href="https://github.com/$1" target="_blank" class="changelogAt">@$1</a>',
);
// Replace list items
text = text.replace(/^\* (.*$)/gm, '<li>$1</li>');
// Wrap list items in <ul></ul>
text = text.replace(/((<li>.*<\/li>\s*)+)/g, '<ul>$1</ul>');
return text;
}; };
const getUpdate = async () => { // Show offline error message if offline
const releases = await fetch( if (isOffline) {
`https://api.github.com/repos/${variables.constants.ORG_NAME}/${variables.constants.REPO_NAME}/releases`,
{
signal: controllerRef.current.signal,
},
);
if (controllerRef.current.signal.aborted === true) {
return;
}
// get the release which tag_name is the same as the current version
const data = await releases.json();
let release = data.find((release) => release.tag_name === variables.constants.VERSION);
if (!release) {
release = data[0];
}
// request the changelog
const res = await fetch(release.url, { signal: controllerRef.current.signal });
if (res.status === 404) {
setError(true);
return;
}
if (controllerRef.current.signal.aborted === true) {
return;
}
const changelog = await res.json();
setTitle(changelog.name);
setContent(parseMarkdown(changelog.body));
setDate(new Date(changelog.published_at).toLocaleDateString());
};
useEffect(() => {
if (navigator.onLine === false || offlineMode) {
return;
}
getUpdate();
return () => {
// stop making requests
controllerRef.current.abort();
};
}, []);
const errorMessage = (msg) => {
return ( return (
<div className="emptyItems"> <div className="emptyItems">
<div className="emptyMessage">{msg}</div> <div className="emptyMessage">
</div>
);
};
if (navigator.onLine === false || offlineMode) {
return errorMessage(
<>
<MdOutlineWifiOff /> <MdOutlineWifiOff />
<h1>{variables.getMessage('modals.main.marketplace.offline.title')}</h1> <h1>{variables.getMessage('modals.main.marketplace.offline.title')}</h1>
<p className="description"> <p className="description">
{variables.getMessage('modals.main.marketplace.offline.description')} {variables.getMessage('modals.main.marketplace.offline.description')}
</p> </p>
</>, </div>
); </div>
}
if (error === true) {
return errorMessage(
<>
<MdOutlineWifiOff />
<span className="title">{variables.getMessage('modals.main.error_boundary.title')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.error_boundary.message')}
</span>
</>,
);
}
if (!title) {
return errorMessage(
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>,
); );
} }
return ( return (
<div className="modalInfoPage changelogtab" ref={changelog}> <div style={{ position: 'relative', width: '100%', minHeight: '100vh' }}>
<span className="mainTitle">{title}</span> {isLoading && (
<span className="subtitle">Released on {date}</span> <div className="loaderHolder" style={{
{image && ( position: 'absolute',
<img top: '50%',
draggable={false} left: '50%',
src={image} transform: 'translate(-50%, -50%)',
alt={title} zIndex: 10
className="updateImage" }}>
/> <div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>
)} )}
<div className="updateChangelog" dangerouslySetInnerHTML={{ __html: content }} /> <iframe
<Modal src="http://localhost:3000/blog/changelog?embed=true"
closeTimeoutMS={100} onLoad={handleLoad}
onRequestClose={() => setShowLightbox(false)} scrolling="no"
isOpen={showLightbox} style={{
className="Modal lightBoxModal" width: '100%',
overlayClassName="Overlay resetoverlay" height: '2000px',
ariaHideApp={false} minHeight: '100vh',
> border: 'none',
<Lightbox opacity: isLoading ? 0 : 1,
modalClose={() => setShowLightbox(false)} transition: 'opacity 0.2s ease-in-out',
img={lightboxImg} }}
title="Changelog"
/> />
</Modal>
</div> </div>
); );
}; };

View File

@@ -2,18 +2,31 @@ import variables from 'config/variables';
import { MARKETPLACE_URL } from 'config/constants'; import { MARKETPLACE_URL } from 'config/constants';
import { memo, useEffect, useRef, useState } from 'react'; import { memo, useEffect, useRef, useState } from 'react';
import { MdOutlineWifiOff } from 'react-icons/md'; import { MdOutlineWifiOff } from 'react-icons/md';
import Modal from 'react-modal';
import Tabs from 'components/Elements/MainModal/backend/Tabs'; import Tabs from 'components/Elements/MainModal/backend/Tabs';
import { useMarketplaceInstall } from 'features/marketplace/components/hooks/useMarketplaceInstall'; import { useMarketplaceInstall } from 'features/marketplace/components/hooks/useMarketplaceInstall';
import Lightbox from 'features/marketplace/components/Elements/Lightbox/Lightbox';
function DiscoverContent({ category, onBreadcrumbsChange }) { function DiscoverContent({ category, onBreadcrumbsChange }) {
const iframeRef = useRef(null); const iframeRef = useRef(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showLightbox, setShowLightbox] = useState(false);
const [lightboxImg, setLightboxImg] = useState(null);
const { installItem, uninstallItem } = useMarketplaceInstall(); const { installItem, uninstallItem } = useMarketplaceInstall();
// Check for offline mode // Check for offline mode
const offlineMode = localStorage.getItem('offlineMode') === 'true'; const offlineMode = localStorage.getItem('offlineMode') === 'true';
const isOffline = navigator.onLine === false || offlineMode; const isOffline = navigator.onLine === false || offlineMode;
// Clear breadcrumbs when component unmounts (navigating away from discover)
useEffect(() => {
return () => {
if (onBreadcrumbsChange) {
onBreadcrumbsChange([]);
}
};
}, [onBreadcrumbsChange]);
useEffect(() => { useEffect(() => {
// Show loader when category changes // Show loader when category changes
setIsLoading(true); setIsLoading(true);
@@ -84,7 +97,8 @@ function DiscoverContent({ category, onBreadcrumbsChange }) {
// Listen for postMessage events from the iframe // Listen for postMessage events from the iframe
const handleMessage = (event) => { const handleMessage = (event) => {
// Verify the origin if needed // Verify the origin if needed
if (event.origin !== MARKETPLACE_URL) { const marketplaceOrigin = new URL(MARKETPLACE_URL).origin;
if (event.origin !== marketplaceOrigin) {
return; return;
} }
@@ -109,7 +123,7 @@ function DiscoverContent({ category, onBreadcrumbsChange }) {
case 'marketplace:item:uninstall': case 'marketplace:item:uninstall':
if (payload?.item) { if (payload?.item) {
uninstallItem(payload.item.type, payload.item.display_name || payload.item.name); uninstallItem(payload.item.type, payload.item.name || payload.item.display_name);
// Send confirmation back to iframe // Send confirmation back to iframe
if (iframeRef.current?.contentWindow) { if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage( iframeRef.current.contentWindow.postMessage(
@@ -148,6 +162,13 @@ function DiscoverContent({ category, onBreadcrumbsChange }) {
} }
break; break;
case 'marketplace:lightbox':
if (payload?.photo) {
setLightboxImg(payload.photo.url);
setShowLightbox(true);
}
break;
default: default:
break; break;
} }
@@ -181,10 +202,23 @@ function DiscoverContent({ category, onBreadcrumbsChange }) {
return ( return (
<div style={{ position: 'relative', width: '100%', minHeight: '100vh' }}> <div style={{ position: 'relative', width: '100%', minHeight: '100vh' }}>
<Modal
closeTimeoutMS={300}
onRequestClose={() => setShowLightbox(false)}
isOpen={showLightbox}
className="Modal lightBoxModal"
overlayClassName="Overlay"
ariaHideApp={false}
>
<Lightbox
modalClose={() => setShowLightbox(false)}
img={lightboxImg}
/>
</Modal>
{isLoading && ( {isLoading && (
<div className="loaderHolder" style={{ <div className="loaderHolder" style={{
position: 'absolute', position: 'absolute',
top: '50%', top: '20%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: 10 zIndex: 10
@@ -200,12 +234,11 @@ function DiscoverContent({ category, onBreadcrumbsChange }) {
scrolling="no" scrolling="no"
style={{ style={{
width: '100%', width: '100%',
height: '2000px', height: '1500px',
minHeight: '100vh', minHeight: '100vh',
border: 'none', border: 'none',
opacity: isLoading ? 0 : 1, opacity: isLoading ? 0 : 1,
transition: 'opacity 0.2s ease-in-out', transition: 'opacity 0.2s ease-in-out',
overflow: 'hidden',
}} }}
title="Marketplace" title="Marketplace"
/> />