UI - ЛАУНЧЕР: LIVE PREVIEW МЕДИА СЕРВИСОВ
This commit is contained in:
parent
a53f286860
commit
2129ffe336
|
|
@ -2952,9 +2952,10 @@ code {
|
||||||
|
|
||||||
.service-media-control {
|
.service-media-control {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 3.35rem;
|
min-height: 3.35rem;
|
||||||
|
gap: 0.22rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--launcher-radius-circle);
|
border-radius: var(--launcher-radius-circle);
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
|
@ -3018,6 +3019,34 @@ code {
|
||||||
gap: 0.12rem;
|
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 {
|
.service-media-source-button {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 2.78rem;
|
width: 2.78rem;
|
||||||
|
|
|
||||||
|
|
@ -1487,20 +1487,42 @@ function ServiceContentModal({
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState<Service>(service);
|
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 [uploadingSlot, setUploadingSlot] = useState<"cover" | "ambient" | null>(null);
|
||||||
const [storageError, setStorageError] = useState<string | null>(null);
|
const [storageError, setStorageError] = useState<string | null>(null);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDraft(service);
|
setDraft(service);
|
||||||
|
setMediaPreviewUrls({
|
||||||
|
cover: service.coverImageUrl ?? null,
|
||||||
|
ambient: service.ambientVideoUrl ?? null,
|
||||||
|
});
|
||||||
setStorageError(null);
|
setStorageError(null);
|
||||||
setUploadingSlot(null);
|
setUploadingSlot(null);
|
||||||
}, [service]);
|
}, [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]) {
|
function update<K extends keyof Service>(key: K, value: Service[K]) {
|
||||||
setDraft((current) => ({ ...current, [key]: value }));
|
setDraft((current) => ({ ...current, [key]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMediaPreview(slot: "cover" | "ambient", previewUrl: string | null) {
|
||||||
|
setMediaPreviewUrls((current) => ({ ...current, [slot]: previewUrl }));
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCoverUpload(file?: File) {
|
async function handleCoverUpload(file?: File) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
await uploadServiceMedia(file, "cover");
|
await uploadServiceMedia(file, "cover");
|
||||||
|
|
@ -1512,6 +1534,8 @@ function ServiceContentModal({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadServiceMedia(file: File, slot: "cover" | "ambient") {
|
async function uploadServiceMedia(file: File, slot: "cover" | "ambient") {
|
||||||
|
const localPreviewUrl = URL.createObjectURL(file);
|
||||||
|
updateMediaPreview(slot, localPreviewUrl);
|
||||||
setStorageError(null);
|
setStorageError(null);
|
||||||
setUploadingSlot(slot);
|
setUploadingSlot(slot);
|
||||||
|
|
||||||
|
|
@ -1532,6 +1556,7 @@ function ServiceContentModal({
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStorageError(error instanceof Error ? error.message : "Не удалось сохранить файл в storage");
|
setStorageError(error instanceof Error ? error.message : "Не удалось сохранить файл в storage");
|
||||||
|
updateMediaPreview(slot, slot === "cover" ? (draft.coverImageUrl ?? null) : (draft.ambientVideoUrl ?? null));
|
||||||
} finally {
|
} finally {
|
||||||
setUploadingSlot(null);
|
setUploadingSlot(null);
|
||||||
}
|
}
|
||||||
|
|
@ -1588,12 +1613,15 @@ function ServiceContentModal({
|
||||||
value={draft.coverImageUrl ?? ""}
|
value={draft.coverImageUrl ?? ""}
|
||||||
fileName={draft.coverMediaFileName ?? null}
|
fileName={draft.coverMediaFileName ?? null}
|
||||||
isUploading={uploadingSlot === "cover"}
|
isUploading={uploadingSlot === "cover"}
|
||||||
|
previewSrc={mediaPreviewUrls.cover}
|
||||||
|
previewKind={draft.coverMediaKind}
|
||||||
onSourceChange={(source) => update("coverMediaSource", source)}
|
onSourceChange={(source) => update("coverMediaSource", source)}
|
||||||
onUrlChange={(value) => {
|
onUrlChange={(value) => {
|
||||||
update("coverImageUrl", value || null);
|
update("coverImageUrl", value || null);
|
||||||
update("coverMediaSource", "url");
|
update("coverMediaSource", "url");
|
||||||
update("coverMediaKind", mediaKindFromUrl(value));
|
update("coverMediaKind", mediaKindFromUrl(value));
|
||||||
update("coverMediaFileName", null);
|
update("coverMediaFileName", null);
|
||||||
|
updateMediaPreview("cover", value || null);
|
||||||
}}
|
}}
|
||||||
onFileChange={handleCoverUpload}
|
onFileChange={handleCoverUpload}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1605,24 +1633,19 @@ function ServiceContentModal({
|
||||||
value={draft.ambientVideoUrl ?? ""}
|
value={draft.ambientVideoUrl ?? ""}
|
||||||
fileName={draft.ambientMediaFileName ?? null}
|
fileName={draft.ambientMediaFileName ?? null}
|
||||||
isUploading={uploadingSlot === "ambient"}
|
isUploading={uploadingSlot === "ambient"}
|
||||||
|
previewSrc={mediaPreviewUrls.ambient}
|
||||||
|
previewKind={draft.ambientMediaKind}
|
||||||
onSourceChange={(source) => update("ambientMediaSource", source)}
|
onSourceChange={(source) => update("ambientMediaSource", source)}
|
||||||
onUrlChange={(value) => {
|
onUrlChange={(value) => {
|
||||||
update("ambientVideoUrl", value || null);
|
update("ambientVideoUrl", value || null);
|
||||||
update("ambientMediaSource", "url");
|
update("ambientMediaSource", "url");
|
||||||
update("ambientMediaKind", mediaKindFromUrl(value));
|
update("ambientMediaKind", mediaKindFromUrl(value));
|
||||||
update("ambientMediaFileName", null);
|
update("ambientMediaFileName", null);
|
||||||
|
updateMediaPreview("ambient", value || null);
|
||||||
}}
|
}}
|
||||||
onFileChange={handleAmbientUpload}
|
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}
|
{storageError ? <p className="service-content-storage-error">{storageError}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2257,6 +2280,8 @@ function MediaSourceField({
|
||||||
value,
|
value,
|
||||||
fileName,
|
fileName,
|
||||||
isUploading = false,
|
isUploading = false,
|
||||||
|
previewSrc,
|
||||||
|
previewKind,
|
||||||
onSourceChange,
|
onSourceChange,
|
||||||
onUrlChange,
|
onUrlChange,
|
||||||
onFileChange,
|
onFileChange,
|
||||||
|
|
@ -2267,6 +2292,8 @@ function MediaSourceField({
|
||||||
value: string;
|
value: string;
|
||||||
fileName?: string | null;
|
fileName?: string | null;
|
||||||
isUploading?: boolean;
|
isUploading?: boolean;
|
||||||
|
previewSrc?: string | null;
|
||||||
|
previewKind?: MediaKind | null;
|
||||||
onSourceChange: (source: ServiceMediaSource) => void;
|
onSourceChange: (source: ServiceMediaSource) => void;
|
||||||
onUrlChange: (value: string) => void;
|
onUrlChange: (value: string) => void;
|
||||||
onFileChange: (file?: File) => void | Promise<void>;
|
onFileChange: (file?: File) => void | Promise<void>;
|
||||||
|
|
@ -2276,7 +2303,7 @@ function MediaSourceField({
|
||||||
const fileTitle = isUploading ? undefined : (fileName ?? "Файл не выбран");
|
const fileTitle = isUploading ? undefined : (fileName ?? "Файл не выбран");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="service-content-field service-media-field">
|
<div className="service-content-field service-content-field--wide service-media-field">
|
||||||
<span>
|
<span>
|
||||||
{icon} {label}
|
{icon} {label}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -2315,6 +2342,9 @@ function MediaSourceField({
|
||||||
<Globe2 size={15} />
|
<Globe2 size={15} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="service-media-preview" aria-hidden="true">
|
||||||
|
{previewSrc ? <MediaPreview src={previewSrc} kind={previewKind} /> : icon}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue