diff --git a/package-lock.json b/package-lock.json index 2382460..33a0c8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1913,6 +1913,126 @@ "glob": "10.3.10" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.17.tgz", + "integrity": "sha512-WiOf5nElPknrhRMTipXYTJcUz7+8IAjOYw3vXzj3BYRcVY0hRHKWgTgQ5439EvzQyHEko77XK+yN9x9OJ0oOog==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.17.tgz", + "integrity": "sha512-29y425wYnL17cvtxrDQWC3CkXe/oRrdt8ie61S03VrpwpPRI0XsnTvtKO06XCisK4alaMnZlf8riwZIbJTaSHQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.17.tgz", + "integrity": "sha512-SSHLZls3ZwNEHsc+d0ynKS+7Af0Nr8+KTUBAy9pm6xz9SHkJ/TeuEg6W3cbbcMSh6j4ITvrjv3Oi8n27VR+IPw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.17.tgz", + "integrity": "sha512-VFge37us5LNPatB4F7iYeuGs9Dprqe4ZkW7lOEJM91r+Wf8EIdViWHLpIwfdDXinvCdLl6b4VyLpEBwpkctJHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.17.tgz", + "integrity": "sha512-aaQlpxUVb9RZ41adlTYVQ3xvYEfBPUC8+6rDgmQ/0l7SvK8S1YNJzPmDPX6a4t0jLtIoNk7j+nroS/pB4nx7vQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.17.tgz", + "integrity": "sha512-HSyEiFaEY3ay5iATDqEup5WAfrhMATNJm8dYx3ZxL+e9eKv10XKZCwtZByDoLST7CyBmyDz+OFJL1wigyXeaoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.17.tgz", + "integrity": "sha512-h5qM9Btqv87eYH8ArrnLoAHLyi79oPTP2vlGNSg4CDvUiXgi7l0+5KuEGp5pJoMhjuv9ChRdm7mRlUUACeBt4w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.17", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.17.tgz", + "integrity": "sha512-BD/G++GKSLexQjdyoEUgyo5nClU7er5rK0sE+HlEqnldJSm96CIr/+YOTT063LVTT/dUOeQsNgp5DXr86/K7/A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-win32-x64-msvc": { "version": "14.2.17", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.17.tgz", diff --git a/public/static/backgrounds/background_amoled.svg b/public/static/backgrounds/background_amoled.svg new file mode 100644 index 0000000..d0fecae --- /dev/null +++ b/public/static/backgrounds/background_amoled.svg @@ -0,0 +1,241 @@ + + + + \ No newline at end of file diff --git a/public/static/backgrounds/background_default.svg b/public/static/backgrounds/background_default.svg new file mode 100644 index 0000000..c5bc7fd --- /dev/null +++ b/public/static/backgrounds/background_default.svg @@ -0,0 +1,241 @@ + + + + \ No newline at end of file diff --git a/src/app/modules/components/card.module.tsx b/src/app/modules/components/card.module.tsx index 6d0d291..2b7cd8b 100644 --- a/src/app/modules/components/card.module.tsx +++ b/src/app/modules/components/card.module.tsx @@ -10,6 +10,8 @@ import { CSSProperties, useEffect, useState } from "react"; import { IconCircleHalf2, IconStar, IconStarFilled, IconUser } from '@tabler/icons-react'; import { getIcon } from "../utils/categories.module"; import { useRouter } from "next/navigation"; +import { Tooltip, UseGlobalTooltip } from "./tooltip"; +import useCookie from "../utils/useCookie.module"; export const formatDate = (date: Date) => { @@ -39,18 +41,62 @@ export const CategoryEl = ({ category, enabled, onClick, hoverable, style }: Cat ); } +const constrainedText = (string: string, max_length: number): string => { + const words = string.split(' '); + for (let x = 0; x < words.length; x++) { + if (words[0].length > max_length) { + return string.slice(0, max_length) + '...'; + } + if (words.slice(0, x).join(' ').length > max_length) { + return words.slice(0, x - 1).join(' ') + '...'; + } + } + return string; +} + +interface CategoryShortenProps { + category: Category, + style?: CSSProperties, + parent_id: string +} + +export const CategoryShorten = ({ category, style, parent_id }: CategoryShortenProps) => { + return ( + +
+ {getIcon(category.icon)} +
+
+ ); +} + export const constrain = (val: number, min_val: number, max_val: number) => { return Math.min(max_val, Math.max(min_val, val)) } +const backgrounds: { [key: string]: string } = { + amoled: 'amoled', + default: 'default' +} + export const Card = ({ el, base64, className }: { el: Bandage, base64: string, className?: { readonly [key: string]: string; } }) => { const [starred, setStarred] = useState(el.starred); const [last, setLast] = useState(el.starred); - const logged = getCookie("sessionId"); + const logged = getCookie('sessionId'); + const theme = useCookie('theme_main'); const router = useRouter(); + const background = backgrounds[theme] ?? 'default'; - const categories = el.categories.map(category => ); + const categories = el.categories.map(category => +
+ +
+ ); useEffect(() => { if (logged && starred != last) { @@ -69,8 +115,10 @@ export const Card = ({ el, base64, className }: { el: Bandage, base64: string, c const StarIcon = starred ? IconStarFilled : IconStar; return ( -
- +
}
- - - +
+ + + +
{categories}
+
{el.title} -

{el.description}

-
{categories}
+

{constrainedText(el.description ?? '', 50)}

diff --git a/src/app/modules/components/tooltip.tsx b/src/app/modules/components/tooltip.tsx index 9e02262..2fc41d1 100644 --- a/src/app/modules/components/tooltip.tsx +++ b/src/app/modules/components/tooltip.tsx @@ -41,6 +41,7 @@ export const Tooltip = ({ body, children, timeout = 800, className, parent_id, o if (parent_id) { const el = document.getElementById(parent_id); const rect = el.getBoundingClientRect(); + console.log(rect.x, rect.y) position_x -= rect.x; position_y -= rect.y } @@ -72,3 +73,50 @@ export const Tooltip = ({ body, children, timeout = 800, className, parent_id, o
); }; + + +interface GlobalTooltipProps { + text: string, + children: JSX.Element, + className?: string, + opacity?: string +} + +export const UseGlobalTooltip = ({ text, children, className, opacity = ".9" }: GlobalTooltipProps) => { + const handleMouseEnter = () => { + const element = document.createElement('span'); + element.id = 'global_tooltip'; + element.innerText = text; + element.className = `${Style.tooltipStyle} ${Style.globalTooltipStyle}`; + element.style.opacity = opacity; + document.body.insertBefore(element, document.body.firstChild); + }; + + const handleMouseLeave = () => { + const tooltip = document.getElementById('global_tooltip'); + tooltip && document.body.removeChild(tooltip); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + const position_x = e.clientX + 10; + const position_y = e.clientY + 10; + + const tooltip = document.getElementById('global_tooltip'); + if (tooltip) { + tooltip.style.left = position_x.toString() + 'px'; + tooltip.style.top = position_y.toString() + 'px'; + } + }; + + return ( + +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/app/styles/tooltip.module.css b/src/app/styles/tooltip.module.css index 589798c..f0471c1 100644 --- a/src/app/styles/tooltip.module.css +++ b/src/app/styles/tooltip.module.css @@ -4,13 +4,19 @@ background-color: var(--main-card-color); color: var(--main-text-color); border-radius: 5px; - padding: 0.5%; + padding: .5rem; z-index: 99999; cursor: pointer; border-top-left-radius: 0%; opacity: .9; box-sizing: border-box; box-shadow: rgba(0, 0, 0, .25) 0px 2px 4px 0px; + border: 1px var(--category-color) solid; +} + +.globalTooltipStyle { + pointer-events: none; + opacity: 0; } @media(max-width: 425px) { diff --git a/src/app/styles/workshop/card.module.css b/src/app/styles/workshop/card.module.css index 424bb1b..23ff3bb 100644 --- a/src/app/styles/workshop/card.module.css +++ b/src/app/styles/workshop/card.module.css @@ -3,11 +3,14 @@ display: flex; flex-direction: column; - background-color: var(--main-card-color); border-radius: 10px; border: 1px solid var(--main-element-color); position: relative; + + background-color: var(--main-card-color) !important; + background-size: 50% !important; + background-repeat: repeat !important; } .skin { @@ -15,6 +18,8 @@ width: 100%; height: auto; box-shadow: var(--main-shadow-color) 0 0px 5px 0px; + border: 1px solid var(--main-element-color); + box-sizing: border-box; transition: box-shadow 200ms; } @@ -55,9 +60,15 @@ .categories { display: flex; - flex-wrap: wrap; + flex-wrap: wrap-reverse; gap: 5px; margin-top: .5rem; + + position: absolute; + bottom: 0; + + padding: .5rem; + padding-bottom: .7rem; } .username { @@ -108,4 +119,23 @@ .star { width: 1.5rem; height: 1.5rem; +} + +.category_shorten { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--main-element-color); + padding: .2rem; + border-radius: 5px; + border: 1px var(--category-color) solid; + cursor: pointer; +} + +.tooltip_container { + display: inline; +} + +.tooltip_body { + font-size: .9rem; } \ No newline at end of file diff --git a/src/app/tutorials/bandage/page.md b/src/app/tutorials/bandage/page.md new file mode 100644 index 0000000..1962414 --- /dev/null +++ b/src/app/tutorials/bandage/page.md @@ -0,0 +1,21 @@ +# Новый способ загрузки повязок + +Обновленный метод загрузки повязок значительно удобнее предыдущего и автоматизирует процесс загрузки повязки с развертки скина. + +### Создание повязки +Для создания повязки можно использовать любые редакторы скинов, такие как Blockbench. Чтобы сайт корректно распознал повязку, её следует размещать на левой руке с широкой (или узкой) моделью. + +Важно: так как движок автоматически определяет высоту повязки, на руке с повязкой не должно быть лишних непрозрачных пикселей. Они могут помешать точному определению высоты. + +### Как это работает? +1. **Определение границ высоты**: + - Сайт анализирует пиксели на руке сверху вниз, проверяя их на прозрачность. + - Как только найден первый непрозрачный пиксель, его позиция фиксируется как начальная высота повязки. + - Затем сайт движется снизу вверх. Первый непрозрачный пиксель снизу фиксируется как конечная высота повязки. +2. **Вычисление высоты**: + Разница между конечной и начальной высотами определяет высоту повязки. +3. **Обработка повязки**: + Найденный диапазон преобразуется в формат, понятный движку. + +### Особенности для разных типов рук +Загружая развертки скинов на сайт стоит учитывать, что для каждого типа рук должна быть соответствующая развертка. \ No newline at end of file diff --git a/src/app/tutorials/bandage/page.tsx b/src/app/tutorials/bandage/page.tsx index c084c7d..35daa4e 100644 --- a/src/app/tutorials/bandage/page.tsx +++ b/src/app/tutorials/bandage/page.tsx @@ -8,7 +8,7 @@ import ASide from "@/app/tutorials/header.module"; import { CustomLink } from "@/app/modules/components/search.module"; import InfoCard from "@/app/modules/components/info.module"; import styleLink from '@/app/styles/tutorials/common.module.css'; -import { IconBulb } from '@tabler/icons-react'; +import { IconAlertTriangle, IconBulb } from '@tabler/icons-react'; export default function Home() { return ( @@ -29,6 +29,49 @@ export default function Home() {

Старайтесь не копировать уже существующие повязки. Например, если ваша работа дублирует один цвет уже существующей окрашиваемой повязки, или если она имеет незначительные различия с другой работой.

+

Новый способ загрузки повязок

+

Обновленный метод загрузки повязок значительно удобнее предыдущего и автоматизирует процесс загрузки повязки с развертки скина.

+ +

Создание повязки

+

Для создания повязки можно использовать любые редакторы скинов, такие как Blockbench. Чтобы сайт корректно распознал повязку, её следует размещать на левой руке с широкой (или узкой) моделью.

+ + + +

Важно

+
}> +

Так как движок автоматически определяет высоту повязки, на руке с повязкой не должно быть лишних непрозрачных пикселей. Они могут помешать точному определению высоты.

+ + +

Как это работает?

+
    +
  1. +

    Определение границ высоты

    +
      +
    • Сайт анализирует пиксели на руке сверху вниз, проверяя их на прозрачность.
    • +
    • Как только найден первый непрозрачный пиксель, его позиция фиксируется как начальная высота повязки.
    • +
    • Затем алгоритм движется снизу вверх. Первый непрозрачный пиксель снизу фиксируется как конечная высота повязки.
    • +
    +
  2. +
  3. +

    Вычисление высоты

    +

    Разница между конечной и начальной высотами определяет высоту повязки.

    +
  4. +
  5. +

    Обработка повязки

    +

    Найденный диапазон преобразуется в формат, понятный сайту.

    +
  6. +
+ +

Особенности для разных типов рук

+

Загружая развертки скинов на сайт нужно учитывать, что для каждого типа рук повязки должна быть развертка соответствующего типа.

+ + +

Старый метод загрузки повязок

+

Этот метод все еще доступен на странице создания, но его использование в разы сложнее нового. + Однако этот метод позволяет боле точно контролировать процесс сборки повязки. +

+

Строение

Перед началом создания повязки важно понять её строение:

    diff --git a/src/app/workshop/[id]/bandage_engine.module.ts b/src/app/workshop/[id]/bandage_engine.module.ts index 75d815d..69e812b 100644 --- a/src/app/workshop/[id]/bandage_engine.module.ts +++ b/src/app/workshop/[id]/bandage_engine.module.ts @@ -234,13 +234,13 @@ class Client { const height = bandage_canvas.height; - let pepe = crop_pepe(bandage_canvas, this.slim, height, this.body_part); + let pepe = crop_pepe(bandage_canvas, this.slim, height, this.body_part, this.split_types); let cropped_pepe = document.createElement("canvas"); cropped_pepe.width = 16; cropped_pepe.height = height; const ctx_pepe = cropped_pepe.getContext("2d", { willReadFrequently: true }); - let lining = crop_pepe(lining_canvas, this.slim, height, this.body_part); + let lining = crop_pepe(lining_canvas, this.slim, height, this.body_part, this.split_types); let cropped_lining = document.createElement("canvas") as HTMLCanvasElement; cropped_lining.width = 16; cropped_lining.height = height; @@ -337,7 +337,8 @@ export const crop_pepe = ( pepe_canvas: HTMLCanvasElement, slim: boolean, height: number, - body_part: number + body_part: number, + split_types: boolean ): HTMLCanvasElement => { const bandage_canvas = document.createElement("canvas") as HTMLCanvasElement; bandage_canvas.width = 16; @@ -345,8 +346,12 @@ export const crop_pepe = ( const context = bandage_canvas.getContext("2d", { willReadFrequently: true }); if (slim && (body_part === 0 || body_part === 2)) { - context?.drawImage(pepe_canvas, 5, 0, 10, height, 5, 0, 10, height); - context?.drawImage(pepe_canvas, 0, 0, 4, height, 1, 0, 4, height); + if (split_types) { + context?.drawImage(pepe_canvas, 0, 0, 15, height, 0, 0, 15, height); + } else { + context?.drawImage(pepe_canvas, 5, 0, 10, height, 5, 0, 10, height); + context?.drawImage(pepe_canvas, 0, 0, 4, height, 1, 0, 4, height); + } } else { context?.drawImage(pepe_canvas, 0, 0); } diff --git a/src/app/workshop/[id]/client_code.tsx b/src/app/workshop/[id]/client_code.tsx index 4e99a18..6321dd4 100644 --- a/src/app/workshop/[id]/client_code.tsx +++ b/src/app/workshop/[id]/client_code.tsx @@ -7,15 +7,11 @@ import style from "@/app/styles/editor/page.module.css"; import * as Interfaces from "@/app/interfaces"; import { useRouter } from "next/navigation"; -import axios from "axios"; - import Client, { b64Prefix } from "./bandage_engine.module"; import SkinView3D from "@/app/modules/components/skinView.module"; import Header from "@/app/modules/components/header.module"; -import Searcher from "@/app/modules/components/nick_search.module"; import { CategoryEl } from '@/app/modules/components/card.module'; -import NextImage from 'next/image'; import Select from 'react-select'; import debounce from 'lodash.debounce'; import NavigatorEl from '@/app/modules/components/navigator.module'; @@ -30,6 +26,7 @@ import { CSSTransition } from 'react-transition-group'; import { IconDownload, IconPlus, IconChevronDown, IconUser, IconEdit, IconX, IconCheck, IconArchive } from '@tabler/icons-react'; import Slider from '@/app/modules/components/slider.module'; import SlideButton from '@/app/modules/components/slideButton.module'; +import SkinLoad from './skinLoad.module'; const body_part: readonly { value: number, label: String }[] = [ @@ -347,9 +344,11 @@ const Info = ({ el, onClick }: { el: Interfaces.Bandage, onClick(): void }) => { {el.title} {el.description &&

    {el.description}

    } -
    - {categories} -
    + {categories.length > 0 && +
    + {categories} +
    + } {el.author ? el.author.public ? {el.author.name} : @@ -359,157 +358,6 @@ const Info = ({ el, onClick }: { el: Interfaces.Bandage, onClick(): void }) => {
} -interface SkinLoadProps { - onChange(data: { data: string; slim: boolean; cape?: string } | null): void -} - -interface SkinResponse { - data: { - skin: { - data: string, - slim: boolean - }, - cape: string - } -} - -const SkinLoad = ({ onChange }: SkinLoadProps) => { - const [data, setData] = useState<{ data: string; slim: boolean; cape?: string }>(null); - const [loaded, setLoaded] = useState(false); - - const isSlim = (img: HTMLImageElement): boolean => { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - context?.clearRect(0, 0, 64, 64); - context?.drawImage(img, 0, 0, img.width, img.height); - const pixelData = context.getImageData(46, 52, 1, 1).data; - return pixelData[3] !== 255; - } - - const loadSkin = (nickname: string) => { - if (!nickname) { - return; - } - axios.get(process.env.NEXT_PUBLIC_API_URL + `minecraft/skin/${nickname}?cape=true`, { validateStatus: () => true }).then((response) => { - if (response.status !== 200) { - switch (response.status) { - case 404: - setError("Игрок с таким никнеймом не найден!"); - break; - case 429: - setError("Сервера Mojang перегружены, пожалуйста, попробуйте через пару минут"); - break; - default: - setError(`Не удалось получить ник! (${response.status})`); - break; - } - return; - } - - const data = response.data as SkinResponse; - setData({ - data: b64Prefix + data.data.skin.data, - slim: data.data.skin.slim, - cape: data.data.cape - }); - setLoaded(true); - }); - } - - const setError = (err: string) => { - const error = document.getElementById("error"); - if (error) { - error.innerText = err; - } - } - - const clearError = () => { - const error = document.getElementById("error"); - if (error) { - error.innerText = ""; - } - } - - const getData = (file: File) => { - if (!file) return; - const reader = new FileReader(); - - reader.onload = () => { - asyncImage(reader.result as string).then(img => { - if (img.width != 64 || img.height != 64) { - setError('Скин должен иметь размеры 64x64 пикселя'); - return; - } - clearError(); - setData({ - data: reader.result as string, - slim: isSlim(img) - }); - setLoaded(true); - }); - } - reader.readAsDataURL(file); - } - - const ondragover = (evt: React.DragEvent) => { - if (evt.dataTransfer?.items[0].type === "image/png") { - evt.preventDefault(); - const drag_container = document.getElementById("drop_container") as HTMLDivElement; - drag_container.style.borderStyle = "solid"; - } - } - - const ondragleave = () => { - const drag_container = document.getElementById("drop_container") as HTMLDivElement; - drag_container.style.borderStyle = "dashed"; - } - - const ondrop = (evt: React.DragEvent) => { - getData(evt.dataTransfer?.files[0]); - - evt.preventDefault(); - const drag_container = document.getElementById("drop_container") as HTMLDivElement; - drag_container.style.borderStyle = "dashed"; - } - - const onChangeInput = (evt: React.ChangeEvent) => { - getData(evt.target?.files[0]); - evt.target.files = null; - } - - return
-
- loadSkin(evt)} /> - - - {data &&
- -
- } -
- - -
-
-
-} const access_level: readonly { value: number, label: String }[] = [ { value: 0, label: "Ограниченный доступ" }, @@ -517,6 +365,9 @@ const access_level: readonly { value: number, label: String }[] = [ { value: 2, label: "Открытый доступ" } ]; + +const lstrip = (string: string) => string.replace(/^\s+/, ''); + const EditElement = ({ bandage, onClose }: { bandage: Interfaces.Bandage, onClose(): void }) => { const router = useRouter(); const [title, setTitle] = useState(bandage.title); @@ -559,8 +410,18 @@ const EditElement = ({ bandage, onClose }: { bandage: Interfaces.Bandage, onClos } return
{bandage.permissions_level >= 2 ? <> -