mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-05 07:44:38 +00:00
feat: add image crop modal
This commit is contained in:
parent
530fc1aad0
commit
5cde53c1d3
5 changed files with 202 additions and 21 deletions
|
@ -116,8 +116,22 @@ const months = [
|
|||
|
||||
export function unixToDate(unix: number, type: string = "short") {
|
||||
const date = new Date(unix * 1000);
|
||||
if (type === "short") return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear();
|
||||
if (type === "full") return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear() + ", " + date.getHours() + ":" + date.getMinutes();
|
||||
if (type === "short")
|
||||
return (
|
||||
date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear()
|
||||
);
|
||||
if (type === "full")
|
||||
return (
|
||||
date.getDate() +
|
||||
" " +
|
||||
months[date.getMonth()] +
|
||||
" " +
|
||||
date.getFullYear() +
|
||||
", " +
|
||||
date.getHours() +
|
||||
":" +
|
||||
date.getMinutes()
|
||||
);
|
||||
}
|
||||
|
||||
export const getSeasonFromUnix = (unix: number) => {
|
||||
|
@ -148,7 +162,9 @@ export function sinceUnixDate(unixInSeconds: number) {
|
|||
if (dateDifferenceSeconds < 86400) return `${hours} ${hoursName} назад`;
|
||||
if (dateDifferenceSeconds < 2592000) return `${days} ${daysName} назад`;
|
||||
|
||||
return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear();
|
||||
return (
|
||||
date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
export function minutesToTime(min: number) {
|
||||
|
@ -248,3 +264,31 @@ export const SortList = {
|
|||
alphabet_descending: 5,
|
||||
alphabet_ascending: 6,
|
||||
};
|
||||
|
||||
export function b64toBlob(
|
||||
b64Data: string,
|
||||
contentType: string,
|
||||
sliceSize?: number
|
||||
) {
|
||||
contentType = contentType || "";
|
||||
sliceSize = sliceSize || 512;
|
||||
|
||||
var byteCharacters = atob(b64Data);
|
||||
var byteArrays = [];
|
||||
|
||||
for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
var slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
var byteNumbers = new Array(slice.length);
|
||||
for (var i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
var byteArray = new Uint8Array(byteNumbers);
|
||||
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
var blob = new Blob(byteArrays, { type: contentType });
|
||||
return blob;
|
||||
}
|
||||
|
|
115
app/components/CropModal/CropModal.tsx
Normal file
115
app/components/CropModal/CropModal.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
import React, { useRef } from "react";
|
||||
import Cropper, { ReactCropperElement } from "react-cropper";
|
||||
import "cropperjs/dist/cropper.css";
|
||||
import { Button, Modal } from "flowbite-react";
|
||||
import { b64toBlob } from "#/api/utils";
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
setSrc: (src: string) => void;
|
||||
setTempSrc: (src: string) => void;
|
||||
setImageData: (src: string) => void;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
height: number;
|
||||
width: number;
|
||||
aspectRatio: number;
|
||||
guides: boolean;
|
||||
quality: number;
|
||||
forceAspect?: boolean;
|
||||
};
|
||||
|
||||
export const CropModal: React.FC<Props> = (props) => {
|
||||
const cropperRef = useRef<ReactCropperElement>(null);
|
||||
|
||||
const getCropData = () => {
|
||||
if (typeof cropperRef.current?.cropper !== "undefined") {
|
||||
props.setSrc(cropperRef.current?.cropper.getCroppedCanvas().toDataURL());
|
||||
|
||||
let block = cropperRef.current?.cropper
|
||||
.getCroppedCanvas({
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
maxWidth: props.width,
|
||||
maxHeight: props.height,
|
||||
})
|
||||
.toDataURL("image/jpeg", props.quality)
|
||||
.split(";");
|
||||
let contentType = block[0].split(":")[1];
|
||||
let realData = block[1].split(",")[1];
|
||||
|
||||
const blob = b64toBlob(realData, contentType);
|
||||
|
||||
const handleFileRead = (e, fileReader) => {
|
||||
const content = fileReader.result;
|
||||
props.setImageData(content);
|
||||
};
|
||||
|
||||
const handleFileText = (file) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = (e) => {
|
||||
handleFileRead(e, fileReader);
|
||||
};
|
||||
fileReader.readAsText(file);
|
||||
};
|
||||
|
||||
handleFileText(blob);
|
||||
props.setTempSrc("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dismissible
|
||||
show={props.isOpen}
|
||||
onClose={() => props.setIsOpen(false)}
|
||||
size={"7xl"}
|
||||
>
|
||||
<Modal.Header>Обрезать изображение</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Cropper
|
||||
src={props.src}
|
||||
style={{ height: 400, width: "100%" }}
|
||||
responsive={true}
|
||||
// Cropper.js options
|
||||
initialAspectRatio={props.aspectRatio}
|
||||
aspectRatio={props.forceAspect ? props.aspectRatio : undefined}
|
||||
guides={props.guides}
|
||||
ref={cropperRef}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="font-bold text-md">Управление</h2>
|
||||
<p>Тяните за углы что-бы выбрать область</p>
|
||||
<p>
|
||||
Нажмите 2 раза на пустое место, что бы поменять режим выбора области
|
||||
на перемещение и обратно
|
||||
</p>
|
||||
<p>Используйте колёсико мыши что-бы изменить масштаб</p>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color={"blue"}
|
||||
onClick={() => {
|
||||
getCropData();
|
||||
props.setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
color={"red"}
|
||||
onClick={() => {
|
||||
props.setSrc(null);
|
||||
props.setTempSrc(null);
|
||||
props.setImageData(null);
|
||||
props.setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -15,8 +15,8 @@ import {
|
|||
Label,
|
||||
Modal,
|
||||
} from "flowbite-react";
|
||||
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
|
||||
import { CropModal } from "#/components/CropModal/CropModal";
|
||||
|
||||
const fetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
|
@ -41,6 +41,7 @@ export const CreateCollectionPage = () => {
|
|||
|
||||
const [imageData, setImageData] = useState<string>(null);
|
||||
const [imageUrl, setImageUrl] = useState<string>(null);
|
||||
const [tempImageUrl, setTempImageUrl] = useState<string>(null);
|
||||
const [isPrivate, setIsPrivate] = useState(false);
|
||||
const [collectionInfo, setCollectionInfo] = useState({
|
||||
title: "",
|
||||
|
@ -53,6 +54,7 @@ export const CreateCollectionPage = () => {
|
|||
const [addedReleases, setAddedReleases] = useState([]);
|
||||
const [addedReleasesIds, setAddedReleasesIds] = useState([]);
|
||||
const [releasesEditModalOpen, setReleasesEditModalOpen] = useState(false);
|
||||
const [cropModalOpen, setCropModalOpen] = useState(false);
|
||||
|
||||
const collection_id = searchParams.get("id") || null;
|
||||
const mode = searchParams.get("mode") || null;
|
||||
|
@ -78,31 +80,19 @@ export const CreateCollectionPage = () => {
|
|||
}
|
||||
}, [userStore.user]);
|
||||
|
||||
const handleFileRead = (e, fileReader, type) => {
|
||||
const handleFileRead = (e, fileReader) => {
|
||||
const content = fileReader.result;
|
||||
if (type === "URL") {
|
||||
setImageUrl(content);
|
||||
} else {
|
||||
setImageData(content);
|
||||
}
|
||||
setTempImageUrl(content);
|
||||
};
|
||||
|
||||
const handleFilePreview = (file) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = (e) => {
|
||||
handleFileRead(e, fileReader, "URL");
|
||||
handleFileRead(e, fileReader);
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleFileLoad = (file) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = (e) => {
|
||||
handleFileRead(e, fileReader, "TEXT");
|
||||
};
|
||||
fileReader.readAsText(file);
|
||||
};
|
||||
|
||||
function handleInput(e) {
|
||||
const regex = /[^a-zA-Zа-яА-Я0-9_.,:()!? \[\]]/g;
|
||||
setCollectionInfo({
|
||||
|
@ -195,7 +185,7 @@ export const CreateCollectionPage = () => {
|
|||
accept="image/jpg, image/jpeg, image/png"
|
||||
onChange={(e) => {
|
||||
handleFilePreview(e.target.files[0]);
|
||||
handleFileLoad(e.target.files[0]);
|
||||
setCropModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
@ -298,6 +288,20 @@ export const CreateCollectionPage = () => {
|
|||
setReleases={setAddedReleases}
|
||||
setReleasesIds={setAddedReleasesIds}
|
||||
/>
|
||||
<CropModal
|
||||
src={tempImageUrl}
|
||||
setSrc={setImageUrl}
|
||||
setTempSrc={setTempImageUrl}
|
||||
setImageData={setImageData}
|
||||
aspectRatio={600 / 337}
|
||||
guides={false}
|
||||
quality={0.9}
|
||||
isOpen={cropModalOpen}
|
||||
setIsOpen={setCropModalOpen}
|
||||
forceAspect={true}
|
||||
width={600}
|
||||
height={337}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
@ -317,7 +321,7 @@ export const ReleasesEditModal = (props: {
|
|||
|
||||
const url = new URL("/api/search", window.location.origin);
|
||||
url.searchParams.set("page", pageIndex.toString());
|
||||
if (!query) return null
|
||||
if (!query) return null;
|
||||
url.searchParams.set("q", query);
|
||||
return url.toString();
|
||||
};
|
||||
|
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -14,6 +14,7 @@
|
|||
"markdown-to-jsx": "^7.4.7",
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^18",
|
||||
"swiper": "^11.1.4",
|
||||
"swr": "^2.2.5",
|
||||
|
@ -1685,6 +1686,11 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cropperjs": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
|
||||
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -4563,6 +4569,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cropper": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.3.3.tgz",
|
||||
"integrity": "sha512-zghiEYkUb41kqtu+2jpX2Ntigf+Jj1dF9ew4lAobPzI2adaPE31z0p+5TcWngK6TvmWQUwK3lj4G+NDh1PDQ1w==",
|
||||
"dependencies": {
|
||||
"cropperjs": "^1.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"markdown-to-jsx": "^7.4.7",
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^18",
|
||||
"swiper": "^11.1.4",
|
||||
"swr": "^2.2.5",
|
||||
|
|
Loading…
Add table
Reference in a new issue