UI - ЛАУНЧЕР: LIVE PREVIEW МЕДИА СЕРВИСОВ
This commit is contained in:
parent
a53f286860
commit
2129ffe336
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue