diff --git a/packages/app/src/utils/imgUtils.js b/packages/app/src/utils/imgUtils.js new file mode 100644 index 0000000..ffeb256 --- /dev/null +++ b/packages/app/src/utils/imgUtils.js @@ -0,0 +1,45 @@ +const loadImage = (src) => + new Promise((resolve, reject) => { + const img = document.createElement('img'); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + +export const analyzeLogoBrightness = async (logoUrl) => { + if (!logoUrl) return false; + try { + const img = await loadImage(logoUrl); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) return false; + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const {data} = ctx.getImageData(0, 0, canvas.width, canvas.height); + + let blackPixelCount = 0; + let transparentPixelCount = 0; + const totalPixels = data.length / 4; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; + if (a === 0) { + transparentPixelCount++; + continue; + } + if (r === 0 && g === 0 && b === 0) { + blackPixelCount++; + } + } + const visiblePixels = totalPixels - transparentPixelCount; + if (visiblePixels === 0) return false; + return blackPixelCount / visiblePixels > 0.85; + } catch (err) { + console.error('[logoUtils] analyzeLogoBrightness failed', err); + return false; + } +}; diff --git a/packages/app/src/views/Details/Details.js b/packages/app/src/views/Details/Details.js index 5cb42aa..dcb5058 100644 --- a/packages/app/src/views/Details/Details.js +++ b/packages/app/src/views/Details/Details.js @@ -20,6 +20,7 @@ import AddToPlaylistModal from '../../components/AddToPlaylistModal'; import DeleteItemDialog from '../../components/DeleteItemDialog'; import {toSubtitleLanguage, mapRemoteSubtitleOptions} from '../Player/remoteSubtitleUtils'; import {getTmdbId, fetchTmdbSeasonRatings} from '../../services/mdblistApi'; +import {analyzeLogoBrightness} from '../../utils/imgUtils'; import css from './Details.module.less'; @@ -174,6 +175,8 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, onI const [logoFailed, setLogoFailed] = useState(false); const handleLogoError = useCallback(() => setLogoFailed(true), []); const handleToastEnd = useCallback(() => setToastMessage(null), []); + const [invertLogo, setInvertLogo] = useState(false); + const [logoUrl, setLogoUrl] = useState(null); // Refs const pageScrollerRef = useRef(null); @@ -402,6 +405,31 @@ const Details = ({itemId, initialItem, onPlay, onSelectItem, onSelectPerson, onI } }, [isLoading, item]); + useEffect(() => { + if (!item) { + setLogoUrl(null); + return; + } + const url = getLogoUrl(effectiveServerUrl, item, {maxWidth: 400, quality: 90}); + setLogoUrl(url); + }, [item, effectiveServerUrl]); + + useEffect(() => { + if (!logoUrl) { + setInvertLogo(false); + return; + } + let cancelled = false; + analyzeLogoBrightness(logoUrl).then((isDark) => { + if (!cancelled) { + setInvertLogo(isDark); + } + }); + return () => { + cancelled = true; + }; + }, [logoUrl]); + // === HANDLERS === const handlePlay = useCallback(() => { @@ -1047,7 +1075,6 @@ const handleSectionKeyDown = useCallback((ev) => { ? getImageUrl(effectiveServerUrl, backdropId, 'Backdrop', {maxWidth: 1920, quality: 90}) : null; - const logoUrl = getLogoUrl(effectiveServerUrl, item, {maxWidth: 400, quality: 90}); const isEpisode = item.Type === 'Episode'; const isSeries = item.Type === 'Series'; @@ -1988,7 +2015,15 @@ const handleSectionKeyDown = useCallback((ev) => { {/* Title or Logo */}
{logoUrl && !logoFailed ? ( - {item.Name} + {item.Name} ) : (

{item.Name}

)} @@ -2504,4 +2539,4 @@ const handleSectionKeyDown = useCallback((ev) => { ); }; -export default Details; \ No newline at end of file +export default Details;