UI - ЛАУНЧЕР: LIVE PREVIEW МЕДИА СЕРВИСОВ

This commit is contained in:
DCCONSTRUCTIONS 2026-05-08 19:30:50 +03:00
parent a53f286860
commit 2129ffe336
2 changed files with 69 additions and 10 deletions

View File

@ -2952,9 +2952,10 @@ code {
.service-media-control {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: center;
min-height: 3.35rem;
gap: 0.22rem;
overflow: hidden;
border-radius: var(--launcher-radius-circle);
background: rgba(255, 255, 255, 0.06);
@ -3018,6 +3019,34 @@ code {
gap: 0.12rem;
}
.service-media-preview {
position: relative;
display: grid;
width: 2.78rem;
min-width: 2.78rem;
height: 2.78rem;
min-height: 2.78rem;
aspect-ratio: 1;
place-items: center;
overflow: hidden;
border-radius: var(--launcher-radius-circle);
background: rgba(255, 255, 255, 0.055);
color: rgba(255, 255, 255, 0.62);
}
.service-media-preview img,
.service-media-preview video {
position: absolute;
inset: 0;
display: block;
width: 100% !important;
max-width: none !important;
height: 100% !important;
max-height: none !important;
border-radius: inherit;
object-fit: cover;
}
.service-media-source-button {
display: grid;
width: 2.78rem;

View File

@ -1487,20 +1487,42 @@ function ServiceContentModal({
onDelete: () => void;
}) {
const [draft, setDraft] = useState<Service>(service);
const [mediaPreviewUrls, setMediaPreviewUrls] = useState<{ cover: string | null; ambient: string | null }>({
cover: service.coverImageUrl ?? null,
ambient: service.ambientVideoUrl ?? null,
});
const [uploadingSlot, setUploadingSlot] = useState<"cover" | "ambient" | null>(null);
const [storageError, setStorageError] = useState<string | null>(null);
const [deleteOpen, setDeleteOpen] = useState(false);
useEffect(() => {
setDraft(service);
setMediaPreviewUrls({
cover: service.coverImageUrl ?? null,
ambient: service.ambientVideoUrl ?? null,
});
setStorageError(null);
setUploadingSlot(null);
}, [service]);
useEffect(() => {
return () => {
Object.values(mediaPreviewUrls).forEach((previewUrl) => {
if (previewUrl?.startsWith("blob:")) {
URL.revokeObjectURL(previewUrl);
}
});
};
}, [mediaPreviewUrls]);
function update<K extends keyof Service>(key: K, value: Service[K]) {
setDraft((current) => ({ ...current, [key]: value }));
}
function updateMediaPreview(slot: "cover" | "ambient", previewUrl: string | null) {
setMediaPreviewUrls((current) => ({ ...current, [slot]: previewUrl }));
}
async function handleCoverUpload(file?: File) {
if (!file) return;
await uploadServiceMedia(file, "cover");
@ -1512,6 +1534,8 @@ function ServiceContentModal({
}
async function uploadServiceMedia(file: File, slot: "cover" | "ambient") {
const localPreviewUrl = URL.createObjectURL(file);
updateMediaPreview(slot, localPreviewUrl);
setStorageError(null);
setUploadingSlot(slot);
@ -1532,6 +1556,7 @@ function ServiceContentModal({
}
} catch (error) {
setStorageError(error instanceof Error ? error.message : "Не удалось сохранить файл в storage");
updateMediaPreview(slot, slot === "cover" ? (draft.coverImageUrl ?? null) : (draft.ambientVideoUrl ?? null));
} finally {
setUploadingSlot(null);
}
@ -1588,12 +1613,15 @@ function ServiceContentModal({
value={draft.coverImageUrl ?? ""}
fileName={draft.coverMediaFileName ?? null}
isUploading={uploadingSlot === "cover"}
previewSrc={mediaPreviewUrls.cover}
previewKind={draft.coverMediaKind}
onSourceChange={(source) => update("coverMediaSource", source)}
onUrlChange={(value) => {
update("coverImageUrl", value || null);
update("coverMediaSource", "url");
update("coverMediaKind", mediaKindFromUrl(value));
update("coverMediaFileName", null);
updateMediaPreview("cover", value || null);
}}
onFileChange={handleCoverUpload}
/>
@ -1605,24 +1633,19 @@ function ServiceContentModal({
value={draft.ambientVideoUrl ?? ""}
fileName={draft.ambientMediaFileName ?? null}
isUploading={uploadingSlot === "ambient"}
previewSrc={mediaPreviewUrls.ambient}
previewKind={draft.ambientMediaKind}
onSourceChange={(source) => update("ambientMediaSource", source)}
onUrlChange={(value) => {
update("ambientVideoUrl", value || null);
update("ambientMediaSource", "url");
update("ambientMediaKind", mediaKindFromUrl(value));
update("ambientMediaFileName", null);
updateMediaPreview("ambient", value || null);
}}
onFileChange={handleAmbientUpload}
/>
<div className="service-content-preview service-content-preview--image">
{draft.coverImageUrl ? <MediaPreview src={draft.coverImageUrl} kind={draft.coverMediaKind} /> : <ImageIcon size={30} />}
</div>
<div className="service-content-preview service-content-preview--video">
{draft.ambientVideoUrl ? <MediaPreview src={draft.ambientVideoUrl} kind={draft.ambientMediaKind} /> : <Video size={30} />}
</div>
{storageError ? <p className="service-content-storage-error">{storageError}</p> : null}
</div>
@ -2257,6 +2280,8 @@ function MediaSourceField({
value,
fileName,
isUploading = false,
previewSrc,
previewKind,
onSourceChange,
onUrlChange,
onFileChange,
@ -2267,6 +2292,8 @@ function MediaSourceField({
value: string;
fileName?: string | null;
isUploading?: boolean;
previewSrc?: string | null;
previewKind?: MediaKind | null;
onSourceChange: (source: ServiceMediaSource) => void;
onUrlChange: (value: string) => void;
onFileChange: (file?: File) => void | Promise<void>;
@ -2276,7 +2303,7 @@ function MediaSourceField({
const fileTitle = isUploading ? undefined : (fileName ?? "Файл не выбран");
return (
<div className="service-content-field service-media-field">
<div className="service-content-field service-content-field--wide service-media-field">
<span>
{icon} {label}
</span>
@ -2315,6 +2342,9 @@ function MediaSourceField({
<Globe2 size={15} />
</button>
</div>
<div className="service-media-preview" aria-hidden="true">
{previewSrc ? <MediaPreview src={previewSrc} kind={previewKind} /> : icon}
</div>
</div>
</div>
);