Image frame lobby

This commit is contained in:
Oznobys 2025-07-14 15:42:49 +03:00
parent cff835795a
commit 53394d2e72
19 changed files with 533 additions and 0 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
src/plugins/ImageFrame/data
src/plugins/ImageFrame/players
src/plugins/ImageFrame/upload
src/map-color-cache.dat

6
.gitignore vendored
View File

@ -9,6 +9,12 @@ world_the_end
./.console_history ./.console_history
src/plugins/ImageFrame/data
src/plugins/ImageFrame/players
src/plugins/ImageFrame/upload
src/map-color-cache.dat
banned-ips.json banned-ips.json
banned-players.json banned-players.json
ops.json ops.json

BIN
plugins/ImageFrame-1.8.4.0.jar (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,190 @@
Messages:
Reloaded: '&eImageFrame has been reloaded!'
ImageMapProcessing: '&eImageMap is being processed, please wait!'
# Leave this empty ("") to disable action bar message
ImageMapProcessingActionBar: '&eImageMap &a{Name} &eis being processed{Dots}'
# Leave this empty ("") to disable action bar message
ImageMapQueuedActionBar: '&7ImageMap &a{Name} &eis currently queued ({Position} Remaining)'
ImageMapCreated: '&aImageMap has been created!'
ImageMapRefreshed: '&aImageMap has been refreshed!'
ImageMapDeleted: '&eImageMap had been deleted!'
ImageMapRenamed: '&aImageMap had been renamed!'
ImageMapTogglePaused: '&aToggled ImageMap playback pause!'
ImageMapPlaybackJumpTo: '&aJumped to position at {Seconds} seconds!'
ImageMapPlayerPurge: '&ePurged {Amount} ImageMaps owned by {CreatorName} ({CreatorUUID}) - [{ImageMapNames}]'
SetCreator: '&aImageMap creator of id {ImageID} set to {CreatorName} ({CreatorUUID})'
InvalidOverlayMap: '&cOverlay only works on Vanilla Minecraft maps and without duplicates in selection!'
ImageMapAlreadyQueued: '&cYou already have another ImageMap queued and processing, please wait!'
UnableToLoadMap: '&cImageMap cannot be loaded, there is a problem while reading the image. Contact server administrators to check the server console for more info.'
UnableToChangeImageType: '&cChanging the image type is currently not supported. Please create a new image map instead.'
UnknownError: '&cAn unknown error had occurred.'
ImageOverMaxFileSize: '&cImageMap cannot be loaded as it is over the max file size allowed. ({Size} bytes)'
NotAnImageMap: '&cThat is not an ImageMap'
UploadLink: '&aOpen {URL} on your browser to upload an image, it is valid for 5 minutes.'
UploadExpired: '&cImage upload link had expired, please create a new one.'
URLImageMapInfo:
- '&bImageID: {ImageID}'
- '&aName: {Name}'
- '&eMap Size: {Width} x {Height}'
- '&6Dithering: {DitheringType}'
- '&dCreator: {CreatorName} ({CreatorUUID})'
- '&fAccess: {Access}'
- '&aTime Created: {TimeCreated}'
- '&bMarkers: {Markers}'
- '&eURL: {URL}'
NoPermission: '&cYou do not have permission to do that!'
NoConsole: '&cThis command can only be ran by players!'
PlayerNotFound: '&cThis player cannot be found!'
InvalidUsage: '&cInvalid Usage!'
NotEnoughMaps: '&cYou do not have {Amount} maps!'
Oversize: '&cThat is too big! Max size for a map is {MaxSize}'
URLRestricted: '&cThat URL is restricted and cannot be used to create image maps.'
PlayerCreationLimitReached: '&cYou can only create {Limit} maps at once! Delete some to create new ones'
DuplicateMapName: '&cYou''ve already created an image map with that name!'
MapLookup: '&bList of image maps by {CreatorName} ({CreatorUUID}):'
ItemFrameOccupied: '&cFailed to place or remove some maps on selected ItemFrame, they are either destroyed, replaced, occupied or protected.'
NotEnoughSpace: '&cUnable to place Combined ImageMap as there is not enough room.'
InvalidImageMap: '&cThis image map had likely already been deleted.'
GivenInvisibleFrame: '&aGiven {Amount} invisible item frames to {Player}.'
AccessPermission:
Updated: '&aUpdated access for {PlayerName} ({PlayerUUID}), they now have {Permission} permission.'
Types:
NONE: NONE
GET: GET
MARKER: MARKER
EDIT: EDIT
EDIT_CLONE: EDIT WITH CLONE
ALL: ALL
Selection:
Begin: '&bRight click an Item Frame to select corner 1 and 2'
Clear: '&eLeaving selection mode'
Corner1: '&aSelected Item Frame corner 1'
Corner2: '&aSelected Item Frame corner 2'
Invalid: '&cInvalid selection!'
Oversize: '&cOversize selection! Max size for a map is {MaxSize}'
Success: '&aSelected {Width} x {Height} Item Frames! &eIf any of them are removed/replaced, you will need to select them again.'
NoSelection: '&cYou don''t have a valid selection yet.'
IncorrectSize: '&cYour selection''s size does not match, {Width} x {Height} required.'
Markers:
AddBegin: '&aRight click on an Item Frame containing the map "{Name}" to place marker! &bRun "/imageframe marker cancel" to cancel placement'
AddConfirm: '&aMarker placed!'
Remove: '&eMarker removed!'
Clear: '&eMarkers cleared!'
Cancel: '&eMarker placement cancelled!'
DuplicateName: '&cThere is already a marker with that name!'
NotAMarker: '&cThat is not a valid marker'
NotRenderOnFrameWarning: '&eWarning: This marker type does not render on Item Frames!'
LimitReached: '&cYou can only create {Limit} markers on one map!'
# Date format used where a time based variable is displayed
DateFormat: dd/MM/yyyy HH:mm:ss zzz
Preferences:
Keys:
VIEW_ANIMATED_MAPS: View Animated Maps
Values:
'TRUE': '&aEnabled'
'FALSE': '&cDisabled'
UNSET: '&7Unset'
UpdateMessage: '&ePlayer Preference {Preference}&e has been updated to {Value}&e!'
Settings:
MapItemFormat: '&f{Name} &7({X}, {Y})'
# Whether empty maps are required to create image maps when not in creative
RequireEmptyMaps: true
# Max size of an image map
MaxSize: 30
# When enabled, Only websites from the list below is allowed to be shown through in-game maps
RestrictImageUrl:
Enabled: false
Whitelist:
- https://i.imgur.com
- http://i.imgur.com
- https://storage.googleapis.com
- http://storage.googleapis.com
- https://cdn.discordapp.com
- http://cdn.discordapp.com
- https://media.discordapp.net
- http://media.discordapp.net
- https://textures.minecraft.net
- http://textures.minecraft.net
# If the image is larger than the defined size, it won't be downloaded for preview
# (In Bytes)
MaxImageFileSize: 30000000
# If an image takes more than this time to load, it will be rejected
# (In Seconds)
MaxProcessingTime: 60
# How many images should be processed in parallel
# Updating this option requires a server restart
ParallelProcessingLimit: 1
# Max amount of image maps a player in the following groups can create
# Setting -1 means unlimited
# To add a player to a group, give the permission "imageframe.createlimit.<group>"
# For example "imageframe.createlimit.vip"
# Players with no groups will be treated as "default" unless they have "imageframe.createlimit.unlimited"
PlayerCreationLimit:
default: 10
vip: 15
moderator: 20
# Max amount of markers on an individual map
MapMarkerLimit: 20
CombinedMapItem:
Name: '&bImageMap &f- &f{Name} &7({Width} x {Height})'
Lore:
- '&aRight Click on Item Frames of size {Width} x {Height} to place ImageMap'
- ''
- '&7ImageID: {ImageID}'
- '&7Creator: {CreatorName} ({CreatorUUID})'
- '&7Time Created: {TimeCreated}'
# How many map packets can be sent to a player in 1/20 of a second
# You can turn this down if you are facing network issues
# However maps might take longer to show to a player
# To disable the rate limit, set to -1
MapPacketSendingRateLimit: -1
# Exempt certain map ids from deletion if their ImageFrame map is deleted
# Values can be map ids (For example: "13") or ranges (inclusive) of map ids (For example: "10-13")
ExemptMapIdsFromDeletion:
- '-1'
# This option should only be useful to developers and people who knew what they are doing
MapRenderersContextual: false
# Changing this option requires a restart
HandleAnimatedMapsOnMainThread: false
SendAnimatedMapsOnMainThread: false
# Valid modes are "DYNAMIC" and "MANUAL_PERSISTENT"
# DYNAMIC: load and unload image cache depending on whether a player is viewing
# May use more CPU and image might appear with a slight delay
# MANUAL_PERSISTENT: image cache stay loaded from server start
# May use more memory
# Changing this setting requires a restart
CacheControlMode: DYNAMIC
# Set this to true if you have corrupted 0 size map data in the world folder (not the ImageFrame plugin folder)
# Set this to false if your system's file IO is slow
TryDeleteBlankMapFiles: false
# ImageFrame's convenient upload system where you can upload directly through an embedded web server
UploadService:
# Changing this value requires a restart
Enabled: false
# This is only used for displaying to the end user
DisplayURL: http://change.this.to.your.server.ip.in.the.config
WebServer:
# Address in which the webserver binds to, defaults to 0.0.0.0
# Use 127.0.0.1 to limit to local access
# Changing this value requires a restart
Host: 0.0.0.0
# Port in which the webserver is hosted, make sure it is not blocked by your firewall
# Changing this value requires a restart
Port: 8517
# ImageFrame's built in survival friendly way of making invisible item frames
InvisibleFrame:
# Survival friendly way to get invisible item frames by splashing dropped items with invisibility potions
# Set to -1 for unlimited
# Set to 0 to disable
MaxConversionsPerSplash: 8
# Should empty invisible item frames glow
# Existing frames are not updated until they are interacted with
GlowEmptyFrames: true
Hooks:
ViaVersion:
# Enable this if your players below 1.13 is having network issues
DisableSmoothAnimationForLegacyPlayers: false
# Enable update notifications for admins
Updater: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,59 @@
{
"type": "com.loohp.imageframe.objectholders.URLStaticImageMap",
"index": 0,
"name": "fdfd",
"url": "https://cdn.discordapp.com/attachments/1371494748854030437/1384225931173826721/image_proxy.jpg?ex\u003d686ea95b\u0026is\u003d686d57db\u0026hm\u003de9b4feec38d3ba7f87a3da4a14e0c52ab478061a29e4337216108da905010c95\u0026",
"width": 3,
"height": 3,
"ditheringType": "nearest-color",
"creator": "40427344-abb4-359c-9e1c-4a5d67f3a3a2",
"hasAccess": {},
"creationTime": 1752051597001,
"mapdata": [
{
"mapid": 0,
"image": "0.png",
"markers": []
},
{
"mapid": 1,
"image": "1.png",
"markers": []
},
{
"mapid": 2,
"image": "2.png",
"markers": []
},
{
"mapid": 3,
"image": "3.png",
"markers": []
},
{
"mapid": 4,
"image": "4.png",
"markers": []
},
{
"mapid": 5,
"image": "5.png",
"markers": []
},
{
"mapid": 6,
"image": "6.png",
"markers": []
},
{
"mapid": 7,
"image": "7.png",
"markers": []
},
{
"mapid": 8,
"image": "8.png",
"markers": []
}
]
}

View File

View File

@ -0,0 +1,6 @@
{
"uuid": "40427344-abb4-359c-9e1c-4a5d67f3a3a2",
"preferences": {
"viewAnimatedMaps": "unset"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

@ -0,0 +1,262 @@
<!--
~ This file is part of ImageFrame.
~
~ Copyright (C) 2025. LoohpJames <jamesloohp@gmail.com>
~ Copyright (C) 2025. Contributors
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ImageFrame Upload</title>
<link href="https://fonts.cdnfonts.com/css/minecraftia" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="imageframe.png">
<style>
* {
font-family: 'Minecraftia', sans-serif;
}
body {
display: block;
justify-content: center;
align-items: center;
height: 90vh;
min-height: 90vh;
text-align: center;
background-image: url("background.webp");
}
.title {
color: white;
margin-bottom: 0;
margin-top: 30px;
}
.expire {
margin: 0;
font-size: 14px;
color: #AAAAAA;
}
.name {
color: #FFFF55;
}
.container {
margin: 30px auto 30px auto;
height: 80%;
text-align: center;
background: #D2BF82;
padding: 20px;
border-radius: 10px;
aspect-ratio: 1;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.drop-zone {
width: 100%;
height: calc(100% - 45px);
background: #A25430;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
margin-bottom: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
color: white;
box-shadow: inset 0 0 3px black;
}
.drop-zone img {
max-width: 100%;
max-height: 100%;
display: none;
position: absolute;
object-fit: cover;
}
button {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAAAPCAAAAACe4j/AAAAEuElEQVRIx1WWS5Ibuw5EtddrdRRJJE4iSKrb4c2/AUuy30iK+jEBHCTwAICMCgAKiGhKVQUwKzJHQo4mQGBy7AIvk1E2AQKhFt6bUGD1gILy0DkD4dmzACkGyzCScgFgQmLZr4LpQpC56QB2wgSYE8i1fd6xd1yPHIAKwAs14Undx2LIHK0X6vdFbzTa6CNBLQE8Y5rapn7oAl3KSDKC6cLfL6sLnIVG7ffnyUgvQ6rwLjUZdIVqWz0iEXXCZ+TJsV8feUdoPHDk+a/IeAbq0ldLT59jtHweHwmKS6gFSUYCqKcVsPami1pEnqolkCMBiAS87FXTICp03geYUr8TV8un3KuyC0BfTUzihOCdwjUya1bkT9VWe+AaFOoJzHdtERXRe/eulyEDhC6pjYwr+JQuIDU9/0wQBnJEAkd+9i6IRTJrV7YWnGfwLA6ESphkBoCu88M0YH1CTTOiRUpDdwpGkkP5yL9phhwn+QVxDarAXq513y9bLKgxBGR/CjDyMl4ECd6lO82CaMLbEHGHp1R8ePDERj392ssaCOigNjJ+dXXl6OLUFfVx6dA1BFJLlHpkUHPam095C3QJLIjn6OLIOB3xe0JS85Zd+govg1/z/UhtG3RdVW98yxNp5I0Vtf/U+6y/dJ/yHtSK4q7G9ARscC3/Df/kXTwiT3d6ovjEISLJIVEvBwWeCXlALZTbpy85xqWuf3sNiBBUmYjI+BqN6aNQqDWpipSYf3W1nOV/WALoykjIiCPreSn97UigsjZdPA5s3AmCDIxaVO0Fa1fV/qkaiijPPz/bEeRyLSBDI8K8j37TO8aH7vmOqUUQ1DboYhvqd+FlFGEo+2T75MbLGnjvPeu7Pl521Ap14dcR3h7p75qv4xieqnNDoyCPLt/yKkf5gOUXl+6S3d9Syq/982fz+fhpsm28C5EhJcxsWWQke6KoPMA7tqm7kKcrn6FLhWcoqCNCxDPyQ8wEKR73nIlEX8GpARSJX7/rzeNtWPD/Dl2IwtOYMwV7osGsHYJanzeU7KqIZVCTcrT0IEbyD4d1e1dkBtW2qLhLXK7a1ogDqsjI1DR4PY7nHEqfF9tn8HUJL+eQ/lGOXdwI1++57CqgmDaReM9CZYLAk4D4ymqti4KZI9Htz9OnrcuOhCQEqJfXaxHPAFG/l/ee1GFJPXmPryYUiUGPutnRGXkagmnjHk5RNpm7xxiclm6CjJEQ4+MD4xPmuT9Ef4b3YT3B51a7d4Np76PF0cavuC+SoaH6PlilipEakoJBJETWT3k7Hf+1REPj2fIx95t5ct6u9S5v3KM2fv2Ve29ax9yC98zT2IZot61MHyYtdPF92nguZ29xfcVxSu9Nj5Gl8G3wcVoW8n1Q/f6BHFEAyy5R2aRIdYH8XfV4453vHKcnqarXPD6mdrD0rlVV5xBPF2xuJM5Oo0SdaaqGMjJva7ii6bYv74IzPMo5pGebpu72ghypwTR+WWDuhQOkFijGyAzF3GXh11lEH0hkPINZivpTqLEtVm2j1hVcn/S0//q0es5dbyxOErxKivQ+bHotvzcT3/XztPf20XqG6fJZMePT2X6ts+nlm9LoSYHMiHuJjGXniB7v2fo/5qT4nuxYg6AAAAAASUVORK5CYII=);
border-color: #AAA #565656 #565656 #AAA;
padding: 7px 20px;
font-size: 15px;
cursor: pointer;
text-shadow: 3px 3px #4C4C4C;
outline: 2px solid #000;
color: white;
}
button:hover:enabled {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAAAPCAMAAACMV5AuAAAAVFBMVEVtd7ZteLZverdwerdwe7hxfLhyfbl0frl1f7p2gLt4grx6hLx7hb18hr5+iL+AisCBi8GCjMGEjsKGkMOHkcSIksSKk8WLlMWLlcaMlcaNlsePmMdwBHmVAAAEjElEQVRIx1WW27YbKw5F3Ul2GYTWFAJX+pyd///PfgDsTg0/mSqQ1k08AEAeDkAA7kXyDAeyu2QCWREgCGSvAAbIE/D9U2m8Ji4Drw0CEhP7EMiqBFxmDMBEkHvd5AziDsjoCKQXddUl1nuZgMZrf0PMdj1kgNY+Ay+CJPg8kj1rR3X/GS9kxaoJVFaB6QkxIX5TBX65mpAZGQH3HaoCFMj6/GxuYgDyTsyup1hfxwxVN+F08ANGANyf8jqAPaBpranJvhxV+VV08JGPVScmcL8W4EIuAFWFHMZrUkUfNC3WtIsEaFogMWJRl839MES61w1cjMUsI1fb7GKaoENMOdFNykzXPz2mlwekkXjV4ioO9b15rZWZd4AaCL9cxdSutl8buIOUkd8JTgAy01IKoFoFPhAZr65SbDHgxFIGKi5IpLaKdlafAHxaVWDt2SSZ24LQhEx6aLGyj7RtELDLiA7EiD72ehBiQJo5oPq1nOQMYGAIYoY2AwIrImaA+25P8vZRa0KgqrjnQIaACiom/1G9Sla3GUHVLl/mMl/mxeUPGT0T5seAeeAQtF9WV2F7cX4niJ4DYmBd12oh7jymyRkBuq6M81mQuExHVv313beo9VH3ojfM2zZ8AkTG8X7EeDs6OZw+mrY7E5l/gqQJmZx+YySQAi1/dqQZy0as4FL1d7nrcfd1kjWTX1ZIYESTUCnuPZE7+anrqcxlz3diUX2HowHCf15S3AvxrnhRxeMwtQACGYGKZ7wGjNl7vH73MHdP8s/vSXM0FiRqMnPowfbjRsfe6n7nZ/FGIyfgF6+A/t1XTrcWB3Q42AxkMOfMfuJotRPgqGqFMfB8KO6eN+BEpO8F2eLZ18YO0GUdmEDcXMcNey+XuOfvP5P35guSVxAzWRYUpIoSuZiJrJ/p4zPouw0pwX+6XwpIcyNWEcJ/uf5Svuyx+ncTfhmotEhIBPe/8f961F/jI5aQfSs3WFOwChnZZxPk+HQjZu/WBuDFVwobZnoLISBOdkmNLFPE9iadiImsgTcdTDKA8dj1qBT518Xcg6+6L0Yl/nqCLeH+b46InkCSETTBzEQBRoPEwS9lKVUkpEx426M3oIMHYQLhK/B6jHvgXw6if4+YM4mlJdWDpLw4MhHgjwBZgIt+ZJdAVENOEkiv6mY4yy5nFPjnQmGfGVy0IqP+cmbsWF7Se/rT+5ZBzHPdKfajHW2ouWldPJaoTDJ3GUYTNMV/IyaiPYqQuX0VPXK+ra3Mc8c5Wlkp4D/e5S5htjXU3HnPPJsBXraRM/rSpOMX99zMoVradRl9AMxJdVPIYgf8Rhu0DvLM739A5rmjJ51QcW9SFXjcvR85oYOhSKTsd65pqdKWIV59ZO/7kKTDJLbvYw9XVRIyTXKJdfm4rOjE18yT8IHM/euZa0Z0fLvBSIh7h925QLoXR80WNfnqOHHP7Qk5aj+dTLX4E3hhIkafoGd15/IDT/lPTVSVr233dy6OkJtiLm0yBu+byeE2Yc59pVrDdKyPjxgD4h6r6vcFwpYNHMxVBaE2ApmfyTTjf5jQnWRymG7iAAAAAElFTkSuQmCC);
border-color: #BDC6FF #59639A #59639A #BDC6FF;
}
button:disabled {
cursor: not-allowed;
filter: brightness(0.5);
}
.closed-container {
text-align: center;
height: 100%;
align-content: center;
}
.closed-icon {
width: 120px;
}
.success-title {
color: #55FF55;
margin: 10px;
}
.error-title {
color: #FF5555;
margin: 10px;
}
@media screen and (max-width: 700px) {
.container {
aspect-ratio: unset;
}
}
</style>
</head>
<body>
<h2 class="title">ImageFrame Upload<label id="titlePlayer"></label></h2>
<p class="expire" id="expire">This link expires in ...</p>
<div class="container" id="container">
<div class="drop-zone" id="dropZone">
Drop image here or click to select
<img id="imagePreview" alt="Image Preview" src="">
</div>
<input type="file" id="fileInput" accept="image/png, image/jpeg, image/gif" hidden>
<button id="uploadBtn" disabled>Upload</button>
</div>
<script>
const urlParams = new URLSearchParams(window.location.search);
const user = urlParams.get('user');
const uploadId = urlParams.get('id');
const player = urlParams.get('player');
const expire = urlParams.get('expire');
function showExpired() {
const forPlayer = player ? ` for <span class="name">${player}</span>` : "";
document.body.innerHTML = `
<div class="closed-container">
<img src="imageframe.png" alt="ImageFrame" class="closed-icon">
<h2 class="error-title">This link${forPlayer} had expired!<br>Please create a new one in-game.</h2>
</div>
`;
}
function showError() {
const forPlayer = player ? ` for <span class="name">${player}</span>` : "";
document.body.innerHTML = `
<div class="closed-container">
<img src="imageframe.png" alt="ImageFrame" class="closed-icon">
<h2 class="error-title">There is an error while uploading${forPlayer}.<br>Please create a new one in-game.</h2>
</div>
`;
}
function showSuccess() {
const forPlayer = player ? ` for <span class="name">${player}</span>` : "";
document.body.innerHTML = `
<div class="closed-container">
<img src="imageframe.png" alt="ImageFrame" class="closed-icon">
<h1 class="success-title">Upload successful${forPlayer}!<br>Please continue in-game!</h1>
</div>
`;
}
if (player) {
document.getElementById('titlePlayer').innerHTML = ` for <span class="name">${player}</span>`;
}
if (expire) {
const element = document.getElementById('expire');
const taskId = setInterval(() => {
const now = Date.now();
const diff = Math.floor((expire - now) / 1000);
if (diff < 0) {
clearInterval(taskId);
showExpired();
return;
}
const minutes = Math.floor(diff / 60);
const seconds = (diff % 60).toString().padStart(2, '0');
element.innerHTML = minutes > 0 ? `This link expires in ${minutes}m ${seconds}s` : `This link expires in ${seconds}s`;
}, 500);
}
const dropZone = document.getElementById("dropZone");
const fileInput = document.getElementById("fileInput");
const uploadBtn = document.getElementById("uploadBtn");
const imagePreview = document.getElementById("imagePreview");
let selectedFile = null;
dropZone.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", handleFileSelect);
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.classList.add("highlight");
});
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("highlight"));
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("highlight");
if (e.dataTransfer.files.length) {
handleFileSelect({ target: { files: e.dataTransfer.files } });
}
});
function handleFileSelect(event) {
const file = event.target.files[0];
if (file && ["image/png", "image/jpeg", "image/gif", "image/webp"].includes(file.type)) {
selectedFile = file;
uploadBtn.disabled = false;
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
imagePreview.style.display = "block";
};
reader.readAsDataURL(file);
} else {
alert("Only PNG, JPG, and GIF files are allowed.");
fileInput.value = "";
selectedFile = null;
uploadBtn.disabled = true;
imagePreview.style.display = "none";
}
}
uploadBtn.addEventListener("click", () => {
if (!selectedFile) return;
const formData = new FormData();
formData.append("image", selectedFile);
fetch(`/upload?user=${user}&id=${uploadId}`, {
method: "POST",
body: formData
})
.then(response => response.json())
.then(data => {
if (data.hasOwnProperty("error")) {
showError();
console.log("Upload failed: " + data.error);
} else {
showSuccess();
console.log("Upload successful: " + data.message);
}
})
.catch(error => {
showError();
console.log("Upload failed: " + error);
});
});
</script>
</body>
</html>