// ==UserScript==
// @name Snay.io Public Skins Lib
// @namespace http://tampermonkey.net/
// @version 1.7
// @description Snay.io Public Lib
// @author GravityG
// @match https://www.snay.io/*
// @grant GM_openInTab
// @grant GM_info
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js
// @license MIT
// ==/UserScript==
/* global bootstrap */
(async function () {
'use strict';
// Add Bootstrap CSS to the document head
const bootstrapCSS = document.createElement('link');
bootstrapCSS.rel = 'stylesheet';
bootstrapCSS.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css';
document.head.appendChild(bootstrapCSS);
console.log('Bootstrap:', typeof bootstrap);
// Add custom CSS for styling, animations, and the navbar button
const customCSS = document.createElement('style');
customCSS.innerHTML = `
nav.navbar {
background-color: rgb(43, 48, 53) !important;
color: white;
position: relative;
}
.btn-shine {
position: absolute;
top: 50%;
left: 14.25rem; /* 9.5rem x 1.5 */
transform: translate(-50%, -50%);
padding: 18px 72px; /* 12px 48px x 1.5 */
color: #fff;
background: linear-gradient(to right, #bed7f7 10%, #79aef2 10%, #868686 20%);
background-position: 0;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: shine 3s infinite linear;
animation-fill-mode: forwards;
font-weight: 600;
font-size: 24px; /* 16px x 1.5 */
text-decoration: none;
white-space: nowrap;
font-family: "Poppins", sans-serif;
}
@keyframes shine {
0% {
background-position: 0;
}
60% {
background-position: 215px;
}
100% {
background-position: 215px;
}
}
.search-container {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
padding-right: 22.5px;
}
.skinurl-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
flex-grow: 1;
padding-right: 22.5px;
align-items: flex-start;
align-content: flex-start;
height: auto;
}
.search-container .input {
border: 3px solid transparent;
width: 24.5em;
height: 3.25em;
padding-left: 1.2em;
outline: none;
overflow: hidden;
background-color: #1d2024;
color: white;
border-radius: 15px;
transition: all 0.5s;
font-size: 1.56rem;
font-weight: bold;
}
.search-container .input:hover,
.search-container .input:focus {
border: 3px solid #4A9DEC !important; /* 2px x 1.5 */
box-shadow: 0px 0px 0px 10.5px rgb(74, 157, 236, 20%) !important; /* 7px x 1.5 */
background-color: #1d2024 !important;
}
#mySkinsContainer, #favoritesContainer, #skinsContainer {
display: flex;
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); /* 150px x 1.5 */
gap: 30px; /* 20px x 1.5 */
padding: 30px; /* 20px x 1.5 */
width: 100%;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
margin: 0 auto;
align-content: flex-start;
}
#skinsContainer::-webkit-scrollbar {
width: 12px; /* 8px x 1.5 */
}
#skinsContainer::-webkit-scrollbar-thumb {
background-color: #6c757d !important;
border-radius: 15px !important; /* 10px x 1.5 */
}
#skinsContainer::-webkit-scrollbar-track {
background-color: #1d2024 !important;
}
.skinItem {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transform: scale(0.9);
filter: blur(10px);
transition: all 0.5s ease-in-out;
}
.skinItem::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
width: 213px;
height: 213px;
background-image: var(--bg);
background-size: cover;
background-position: center;
filter: contrast(1.2) brightness(0.6) saturate(1.75) url(#gaussianBlur);
border-radius: 50%;
z-index: -1;
opacity: 0.9;
mask: radial-gradient(circle, rgba(0, 0, 0, 1) 75%, rgba(0, 0, 0, 0) 100%);
}
.skinItem.loaded {
opacity: 1;
transform: scale(1);
filter: blur(0);
}
.skinItem > img {
width: 195px; /* 130px x 1.5 */
height: 195px; /* 130px x 1.5 */
border-radius: 75%; /* 50% x 1.5 */
object-fit: cover !important;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.skinItem img:hover {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
}
.badge {
margin-top: 15px; /* 10px x 1.5 */
font-size: 1.35rem; /* 0.9rem x 1.5 */
text-align: center;
background-color: #0d6efd !important;
color: white !important;
padding: 7.5px 15px; /* 5px 10px x 1.5 */
border-radius: 5px; /* 10px x 1.5 */
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-image {
margin-left: 30px; /* 20px x 1.5 */
width: 50px; /* 40px x 1.5 */
height: 50px; /* 40px x 1.5 */
border-radius: 75%; /* 50% x 1.5 */
cursor: pointer;
border: 3px solid white; /* 2px x 1.5 */
}
.dropdown-menu.custom-dropdown-menu {
min-width: 15rem;
padding: 0.75rem 0;
font-size: 2.25rem;
color: var(--bs-body-color);
background-color: #1d202496 !important;
border-radius: 15px !important;
box-shadow: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.15) !important;
filter: drop-shadow(2px 9px 9px black);
}
.dropdown-menu.custom-dropdown-menu>li>a {
display: block;
padding: 4.5px 30px !important; /* 3px 20px x 1.5 */
clear: both;
font-weight: 400 !important;
color: #fafafa !important;
white-space: nowrap !important;
}
.dropdown-menu.custom-dropdown-menu>li>a:hover {
background-color: #4A9DEC !important;
color: white !important;
}
.dropdown-menu.custom-dropdown-menu .dropdown-item.disabled {
color: #6c757d !important;
cursor: not-allowed !important;
}
.multi-button {
display: flex;
justify-content: center;
margin: 1.5rem auto; /* 1rem x 1.5 */
width: fit-content;
}
.multi-button > button {
font-size: 1.8rem; /* 1.2rem x 1.5 */
padding: 0.75em 1.5em; /* 0.5em 1em x 1.5 */
background: #fff;
color: #4A5568;
border: 0px solid #A0AEC0;
margin: 0.15em; /* 0.1em x 1.5 */
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
box-shadow: 0 0 0 #BEE3F8;
transform: translateY(0);
cursor: pointer;
font-weight: bold;
}
.multi-button > button:first-of-type {
border-radius: 0.75em 0 0 0.75em; /* 0.5em x 1.5 */
}
.multi-button > button:last-of-type {
border-radius: 0 0.75em 0.75em 0; /* 0.5em x 1.5 */
}
.multi-button > button:hover {
background: #D53F8C;
color: #fff;
box-shadow: 0 0 1.2em 0 rgba(213, 63, 140, 0.8); /* 0 0 0.8em x 1.5 */
transform: translateY(-0.3em); /* -0.2em x 1.5 */
}
.multi-button > button.active {
background: #D53F8C !important;
color: white !important;
box-shadow: 0 0 1.2em 0 rgba(213, 63, 140, 0.8) !important; /* 0 0 0.8em x 1.5 */
transform: translateY(-0.3em); /* -0.2em x 1.5 */
}
.button1 {
position: absolute; /* Positions the button relative to its container */
right: 10px; /* Pushes the button to the far right */
width: 4em;
height: 4em;
border: none;
background: rgba(180, 83, 107, 0.11);
border-radius: 5px;
transition: background 0.5s;
display: flex; /* Flexbox ensures centering */
justify-content: center;
align-items: center;
}
.X, .Y {
content: "";
position: absolute;
width: 2em;
height: 2px; /* Adjust thickness for better visibility */
background-color: #fff;
}
.X {
transform: rotate(45deg);
}
.Y {
transform: rotate(-45deg);
}
.button1:hover {
background-color: rgb(211, 21, 21);
}
`;
document.head.appendChild(customCSS);
const customMySkinsCSS = document.createElement('style');
customMySkinsCSS.innerHTML = `
.profile-upload {
width: 150px;
height: 150px;
border-radius: 75%;
cursor: pointer;
border: 3px solid #add8e6;
}
.form-container {
display: flex;
margin-top: 10px !important;
justify-content: center;
align-items: flex-start; !important;
gap: 5px; /* Add space between elements */
margin-top: 0; /* Adjust as needed for spacing from the top */
}
.skin-name-input {
position: relative;
margin: 20px 0;
width: 190px;
}
.skin-name-input input {
background-color: transparent;
border: 0;
border-bottom: 2px #fff solid;
display: block;
width: 100%;
padding: 15px 0;
font-size: 18px;
color: #fff;
}
.skin-name-input input:focus,
.skin-name-input input:valid {
outline: 0;
border-bottom-color: lightblue;
}
.skin-name-input label {
position: absolute;
top: 15px;
left: 0;
pointer-events: none;
}
.skin-name-input label span {
display: inline-block;
font-size: 18px;
min-width: 5px;
color: #fff;
transition: 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.skin-name-input input:focus + label span,
.skin-name-input input:valid + label span {
color: lightblue;
transform: translateY(-30px);
}
.submit-btn {
background-color: lightblue;
border: none;
border-radius: 5px;
color: #000;
font-size: 18px;
padding: 10px 15px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.submit-btn:hover {
background-color: #007acc;
color: #fff;
}
.submit-btn:active {
transform: scale(0.95);
}
.dot-spinner {
--uib-size: 2.8rem;
--uib-speed: .9s;
--uib-color: #183153;
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
height: var(--uib-size);
width: var(--uib-size);
}
.dot-spinner__dot {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: flex-start;
height: 100%;
width: 100%;
}
.dot-spinner__dot::before {
content: '';
height: 20%;
width: 20%;
border-radius: 50%;
background-color: #ffffff;
transform: scale(0);
opacity: 0.5;
animation: pulse0112 calc(var(--uib-speed) * 1.111) ease-in-out infinite;
box-shadow: 0 0 20px rgba(18, 31, 53, 0.3);
}
.dot-spinner__dot:nth-child(2) {
transform: rotate(45deg);
}
.dot-spinner__dot:nth-child(2)::before {
animation-delay: calc(var(--uib-speed) * -0.875);
}
.dot-spinner__dot:nth-child(3) {
transform: rotate(90deg);
}
.dot-spinner__dot:nth-child(3)::before {
animation-delay: calc(var(--uib-speed) * -0.75);
}
.dot-spinner__dot:nth-child(4) {
transform: rotate(135deg);
}
.dot-spinner__dot:nth-child(4)::before {
animation-delay: calc(var(--uib-speed) * -0.625);
}
.dot-spinner__dot:nth-child(5) {
transform: rotate(180deg);
}
.dot-spinner__dot:nth-child(5)::before {
animation-delay: calc(var(--uib-speed) * -0.5);
}
.dot-spinner__dot:nth-child(6) {
transform: rotate(225deg);
}
.dot-spinner__dot:nth-child(6)::before {
animation-delay: calc(var(--uib-speed) * -0.375);
}
.dot-spinner__dot:nth-child(7) {
transform: rotate(270deg);
}
.dot-spinner__dot:nth-child(7)::before {
animation-delay: calc(var(--uib-speed) * -0.25);
}
.dot-spinner__dot:nth-child(8) {
transform: rotate(315deg);
}
.dot-spinner__dot:nth-child(8)::before {
animation-delay: calc(var(--uib-speed) * -0.125);
}
@keyframes pulse0112 {
0%,
100% {
transform: scale(0);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
}
#shine2 {
display: none;
}
@media (max-width: 939px) {
.container-fluid {
flex-direction: column; /* Stack items vertically */
align-items: flex-start; /* Align content to the left */
}
.btn-shine {
display: none;
}
.dropdown-menu.custom-dropdown-menu {
right: -15rem;
}
#shine2 {
display: block;
top: auto;
left: 50%;
}
@media (max-width: 560px) {
.search-container {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
padding-right: 22.5px;
height: 10px;
}
.search-container .input {
height: 2.25em;
}
.profile-image {
margin-left: 30px;
width: 35px;
height: 35px;
border-radius: 75%;
cursor: pointer;
border: 3px solid white;
}
.multi-button {
display: flex;
justify-content: center;
margin: 1.5rem auto;
width: fit-content;
height: 30px;
align-items: center;
}
.dropdown-menu.custom-dropdown-menu {
right: -5rem;
}
.skinItem > img {
width: 155px;
height: 155px;
border-radius: 75%;
object-fit: cover !important;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
#mySkinsContainer, #favoritesContainer, #skinsContainer {
display: flex;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
padding: 30px;
width: 100%;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
margin: 0 auto;
align-content: flex-start;
}
.skinItem::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -63%);
width: 166px;
height: 166px;
background-image: var(--bg);
background-size: cover;
background-position: center;
filter: contrast(1.2) brightness(0.6) saturate(1.75) url(#gaussianBlur);
border-radius: 50%;
z-index: -1;
opacity: 0.9;
mask: radial-gradient(circle, rgba(0, 0, 0, 1) 75%, rgba(0, 0, 0, 0) 100%);
}
}
`;
document.head.appendChild(customMySkinsCSS);
(function () {
// Append CSS to the document for notifications
const notificationCSS = `
.notification {
display: flex;
flex-direction: column;
isolation: isolate;
position: fixed;
bottom: 5%; /* Spawns in the bottom-left corner */
left: 2%;
z-index: 9999;
width: 18rem;
height: auto;
background: #29292c;
border-radius: 1rem;
overflow: hidden;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
font-size: 16px;
--gradient: linear-gradient(to bottom, #2eadff, #3d83ff, #7e61ff);
--color: #32a6ff;
opacity: 0;
transform: translateY(20px) scale(0.9);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateY(0) scale(1);
}
.notification:before {
position: absolute;
content: "";
inset: 0.0625rem;
border-radius: 0.9375rem;
background: #18181b;
z-index: 2;
}
.notification:after {
position: absolute;
content: "";
width: 0.25rem;
inset: 0.65rem auto 0.65rem 0.5rem;
border-radius: 0.125rem;
background: var(--gradient);
transition: transform 300ms ease;
z-index: 4;
}
.notification:hover:after {
transform: translateX(0.15rem);
}
.notititle {
color: var(--color);
padding: 0.65rem 0.25rem 0.4rem 1.25rem;
font-weight: 500;
font-size: 1.1rem;
visibility: visible; /* Ensure visibility */
}
.notibody {
color: #99999d;
padding: 0 1.25rem;
font-size: 0.9rem;
visibility: visible; /* Ensure visibility */
}
.notification.success {
--gradient: linear-gradient(to bottom, #28a745, #218838);
--color: #28a745;
}
.notification.error {
--gradient: linear-gradient(to bottom, #dc3545, #c82333);
--color: #dc3545;
}
.notification.info {
--gradient: linear-gradient(to bottom, #2eadff, #3d83ff);
--color: #32a6ff;
}
`;
// Append the CSS to the document
const style = document.createElement("style");
style.innerHTML = notificationCSS;
document.head.appendChild(style);
// Define the notification system as a window property
window.showNotification = function (type, title = "Title", body = "Body", duration = 3000) {
if (!title || !body) {
console.error("Notification requires a title and body.");
return;
}
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.innerHTML = `
<div class="notititle">${title}</div>
<div class="notibody">${body}</div>
`;
document.body.appendChild(notification);
// Trigger animation
requestAnimationFrame(() => {
notification.classList.add("show");
});
// Auto-remove after duration
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300); // Allow transition to finish
}, duration);
};
// Example usage message in the console
console.log("Notification system loaded. Test it by calling showNotification(type, title, body, duration) from the console.");
})();
// JavaScript for Tab Switching
function setupTabSwitching() {
const publicSkinsTab = document.getElementById('publicSkinsTab');
const mySkinsTab = document.getElementById('mySkinsTab');
const favoritesTab = document.getElementById('favoritesTab');
const skinsContainer = document.getElementById('skinsContainer');
const mySkinsContainer = document.getElementById('mySkinsContainer');
const favoritesContainer = document.getElementById('favoritesContainer');
const title = document.getElementById('shine2');
const title2 = document.getElementById('shine1');
// Function to update tab content
function switchTab(tabKey) {
// Hide all containers by default
skinsContainer.style.display = 'none';
mySkinsContainer.style.display = 'none';
favoritesContainer.style.display = 'none';
if (tabKey === 'public') {
// Show the skins container for PUBLIC SKINS
title.innerText = 'Public Skins';
title2.innerText = 'Public Skins';
skinsContainer.style.display = 'grid';
skinsContainer.innerHTML = `
<div class="dot-spinner">
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
</div>`;
// Fetch and refresh skins when switching to PUBLIC SKINS
fetchImages()
.then((images) => {
renderImagesLazy(images, skinsContainer);
console.log("Public skins refreshed successfully!");
})
.catch((error) => {
skinsContainer.innerHTML = '<p style="color: red;">Failed to refresh skins. Please try again later.</p>';
console.error("Failed to refresh skins:", error);
});
} else if (tabKey === 'mySkins') {
// Display custom content for MY SKINS tab
title.innerText = 'My Skins';
title2.innerText = 'My Skins';
mySkinsContainer.style.display = 'flex';
mySkinsContainer.style.flexDirection = 'column';
mySkinsContainer.style.justifyContent = 'flex-start';
mySkinsContainer.style.alignItems = 'center';
mySkinsContainer.style.marginTop = '10px';
mySkinsContainer.innerHTML = `
<img src="https://i.imghippo.com/files/IFso7215PtQ.png" alt="Preview" class="profile-upload" id="preview" aria-expanded="false">
<div class="search-container skinurl-container">
<input id="skinName" class="input" style="background-color: #454444;" placeholder="Skin name...">
<input id="skinurl" class="input" style="background-color: #454444;" placeholder="Skin url...">
<button id="submitSkin" class="submit-btn"><span>Confirm</span> <span>Skin</span></button>
</div>
`;
// Get the preview image element
const previewImg = document.getElementById('preview');
const urlInput = document.getElementById('skinurl');
const nameInput = document.getElementById('skinName');
// Create a hidden file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.png, .jpeg, .jpg, .gif'; // Restrict to specific file types
fileInput.style.display = 'none'; // Hide the input
// Append the file input to the body
document.body.appendChild(fileInput);
// Add a click event to the preview image
previewImg.addEventListener('click', () => {
fileInput.click(); // Trigger the file input's click
});
// Enable or disable the URL input based on the preview image state
const toggleUrlInput = () => {
if (previewImg.src.startsWith('data:image')) {
urlInput.value = '';
urlInput.disabled = true;
} else {
urlInput.disabled = false;
}
};
previewImg.addEventListener('load', toggleUrlInput);
// Handle file input change
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const validExtensions = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']; // Allowed MIME types
if (validExtensions.includes(file.type)) {
const reader = new FileReader();
reader.onload = (e) => {
previewImg.src = e.target.result; // Update the image src
};
reader.readAsDataURL(file);
} else {
alert('Please select a valid image file (PNG, JPEG, or GIF).');
}
}
});
// Handle skin submission
document.getElementById('submitSkin').addEventListener('click', async () => {
const name = nameInput.value.trim(); // Get and trim the skin name
const owner = protoService?.userInfo?.id || 'defaultOwner'; // Ensure owner ID exists
// Check if an image is selected
const isImageSelected = previewImg.src.startsWith('data:image');
// Validate inputs
if (!name) {
alert('Please provide a skin name.');
return;
}
if (!isImageSelected && !urlInput.value) {
alert('Please provide a URL or select an image.');
return;
}
if (!isImageSelected && !urlInput.value.startsWith('https://i.imgur.com')) {
alert('URL must start with https://i.imgur.com.');
return;
}
try {
let url;
if (isImageSelected) {
console.log('Uploading image to Imgur...');
const base64Image = previewImg.src.split(',')[1]; // Extract Base64 data
// Upload image to Imgur
const imgurResponse = await fetch('https://api.imgur.com/3/image', {
method: 'POST',
headers: {
Authorization: 'Client-ID 8b2cd28516b0976', // Replace with your Imgur Client ID
'Content-Type': 'application/json',
},
body: JSON.stringify({ image: base64Image, type: 'base64' }),
});
if (!imgurResponse.ok) {
const error = await imgurResponse.json();
console.error('Imgur upload failed:', error);
throw new Error('Failed to upload image to Imgur.');
}
const imgurData = await imgurResponse.json();
url = imgurData.data.link; // Get the Imgur URL
console.log('Image uploaded to Imgur:', url);
} else {
url = urlInput.value.trim(); // Get the user-provided URL
}
// Send POST request to the new API endpoint
const response = await fetch('https://snay.vercel.app/api/addSkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, url, owner }), // JSON-encoded body
});
// Check response status
if (!response.ok) {
const errorData = await response.json();
console.error('Server error details:', errorData);
throw new Error(`Error ${response.status}: ${errorData.error || 'Unknown error occurred'}`);
}
const result = await response.json();
console.log('Skin added successfully. Server response:', result);
// Notify the user of success
showNotification("success", "Success!", "Skin uploaded successfully!.");
if (isImageSelected) {
previewImg.src = result.url; // Update the preview image to the uploaded Imgur URL
}
} catch (error) {
console.error('Error during skin submission:', error);
alert(`An error occurred: ${error.message}`);
}
});
} else if (tabKey === 'favorites') {
// Display custom content for FAVORITES tab
title.innerText = 'Favorites';
title2.innerText = 'Favorites';
favoritesContainer.style.display = 'flex';
favoritesContainer.style.justifyContent = 'center';
favoritesContainer.style.alignItems = 'center';
favoritesContainer.innerHTML = `
<div style="text-align: center;">
<img src="https://media.tenor.com/hB9OTbewrikAAAAi/work-work-in-progress.gif"
alt="Work in Progress"
style="max-width: 100%; height: auto; border-radius: 10px;">
</div>`;
}
// Update the active tab button styles
publicSkinsTab.classList.remove('active');
mySkinsTab.classList.remove('active');
favoritesTab.classList.remove('active');
if (tabKey === 'public') publicSkinsTab.classList.add('active');
else if (tabKey === 'mySkins') mySkinsTab.classList.add('active');
else if (tabKey === 'favorites') favoritesTab.classList.add('active');
}
// Add event listeners to each tab button
publicSkinsTab.addEventListener('click', () => switchTab('public'));
mySkinsTab.addEventListener('click', () => switchTab('mySkins'));
favoritesTab.addEventListener('click', () => switchTab('favorites'));
// Set PUBLIC SKINS as the default tab
switchTab('public');
}
// URL of the API endpoint
const API_URL = "https://snay.vercel.app/api/skins";
// Fetch images from the API
async function fetchImages() {
try {
console.log("Starting fetch for skins...");
const response = await fetch(API_URL, {
method: 'GET', // Explicitly specify GET method
});
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`);
}
const { data } = await response.json(); // Extract `data` property from API response
console.log("Fetched skins data:", data);
return data;
} catch (error) {
console.error("Error in fetchImages:", error);
throw error;
}
}
const svgFilter = `
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<filter id="gaussianBlur">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" />
</filter>
</svg>
`;
document.body.insertAdjacentHTML('afterbegin', svgFilter);
// Render skins in a lazy-loaded manner
function renderImagesLazy(images, container) {
container.innerHTML = ""; // Clear previous content
if (!images || images.length === 0) {
container.innerHTML = '<p style="color: white;">No skins found.</p>';
return;
}
// Keep track of the currently open dropdown
let openDropdown = null;
images.forEach((image, index) => {
// Create the main skin container
const skinDiv = document.createElement("div");
skinDiv.className = "skinItem";
skinDiv.style.position = "relative";
skinDiv.style.setProperty("--bg", `url(${image.url})`);
// Create the image element
const img = document.createElement("img");
img.src = image.url;
img.alt = `Owner: ${image.owner}`; // Set the alt attribute to include _id
img.title = image.name; // Set the title to the skin's name
// Create a badge for the skin name
const badge = document.createElement("span");
badge.className = "badge text-bg-secondary";
badge.textContent = image.name;
// Badge click event to call userInfoRequest with owner ID
badge.addEventListener("click", (event) => {
event.stopPropagation(); // Prevent other click handlers from triggering
if (image.owner) {
userInfoRequest(image.owner); // Call userInfoRequest with the owner ID
} else {
console.warn("Owner ID is missing for this skin.");
}
});
// Create the dropdown container
const dropdownContainer = document.createElement("ul");
dropdownContainer.className = "dropdown-menu custom-dropdown-menu dropdown-menu-end";
dropdownContainer.style.cssText = `
display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -200%);
z-index: 1000;
flex-direction: column;
justify-content: center;
align-items: center;
`;
// Create dropdown item for "Vip Skin"
const vipDropdownItem = document.createElement("li");
const vipDropdownLink = document.createElement("a");
vipDropdownLink.className = "dropdown-item";
vipDropdownLink.href = "#";
// Create dropdown item for "Custom Skin"
const customSkinDropdownItem = document.createElement("li");
const customSkinDropdownLink = document.createElement("a");
customSkinDropdownLink.className = "dropdown-item";
customSkinDropdownLink.href = "#";
// Add an image badge next to the dropdown link
const vipBadgeImage = document.createElement("img");
vipBadgeImage.src = "./assets/badges/badge4.png";
vipBadgeImage.alt = "Badge";
vipBadgeImage.className = "badge-image";
vipBadgeImage.style.cssText = `
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: middle;
`;
// Add an image badge next to the dropdown link
const customBadgeImage = document.createElement("img");
customBadgeImage.src = "./assets/img/SkinsCustom.png";
customBadgeImage.alt = "Custom Skin";
customBadgeImage.className = "badge-image";
customBadgeImage.style.cssText = `
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: middle;
`;
vipDropdownLink.appendChild(vipBadgeImage);
vipDropdownLink.appendChild(document.createTextNode("Vip Skin"));
customSkinDropdownLink.appendChild(customBadgeImage);
customSkinDropdownLink.appendChild(document.createTextNode("Custom Skin"));
// Dropdown link click handler
vipDropdownLink.addEventListener("click", (event) => {
event.preventDefault();
const vipSkinInput = document.getElementById("addVipSkin");
if (vipSkinInput) {
vipSkinInput.value = image.url; // Set the skin URL
} else {
console.warn("Input element with id='addVipSkin' not found.");
}
const vipButton = document.querySelector("#gallery-body > div.vip-div > ul > li:nth-child(1) > button");
if (vipButton) {
vipButton.click();
}
});
customSkinDropdownLink.addEventListener("click", (event) => {
event.preventDefault(); // Prevent default link behavior
changeSkin(image.url); // Dynamically pass the URL of the skin
});
vipDropdownItem.appendChild(vipDropdownLink);
dropdownContainer.appendChild(vipDropdownItem);
customSkinDropdownItem.appendChild(customSkinDropdownLink);
dropdownContainer.appendChild(customSkinDropdownItem);
// Dropdown toggle logic
skinDiv.addEventListener("click", (event) => {
event.stopPropagation(); // Prevent click event propagation
if (openDropdown && openDropdown !== dropdownContainer) {
openDropdown.style.display = "none"; // Close other dropdownsF
}
dropdownContainer.style.display =
dropdownContainer.style.display === "block" ? "none" : "block";
openDropdown = dropdownContainer.style.display === "block" ? dropdownContainer : null;
});
// Append elements to the skin container
skinDiv.appendChild(img);
skinDiv.appendChild(badge);
skinDiv.appendChild(dropdownContainer);
// Append skin container to the main container
container.appendChild(skinDiv);
// Apply lazy-loading animation with delay
setTimeout(() => {
skinDiv.classList.add("loaded");
}, index * 100); // 100ms delay per skin
});
// Global click listener to close dropdowns when clicking outside
document.addEventListener("click", () => {
if (openDropdown) {
openDropdown.style.display = "none";
openDropdown = null; // Reset the tracker
}
});
}
// Example usage
const skinsContainer = document.getElementById("skinsContainer");
fetchImages()
.then((images) => renderImagesLazy(images, skinsContainer))
.catch((error) => {
skinsContainer.innerHTML = '<p style="color: red;">Failed to load skins.</p>';
});
// Main Function
async function main() {
const sideButtons = document.querySelector('#main-menu .side-buttons') || await waitForElement('#main-menu .side-buttons');
// Create the "Public Skins" button
const button = document.createElement('button');
button.className = 'btn side-btn';
button.id = 'PublicSkins';
button.style.cssText = `
background-color: rgb(139 92 246 / 0%);
color: white;
border-radius: 0px;
padding: 16px 40px;
font-size: 1.2rem;
background-image: url(https://i.postimg.cc/wBhr5Ftc/SlF1SEF.png);
`;
// Ensure the sideButtons container is set up
sideButtons.style.display = 'flex';
sideButtons.style.flexDirection = 'column';
sideButtons.style.alignItems = 'center';
sideButtons.prepend(button);
// Create the drawer
const drawer = document.createElement('div');
drawer.className = 'offcanvas offcanvas-bottom';
drawer.tabIndex = -1;
drawer.id = 'publicSkinsDrawer';
drawer.setAttribute('aria-labelledby', 'publicSkinsDrawerLabel');
drawer.style.height = '100%';
drawer.style.backgroundColor = '#1d2024';
drawer.innerHTML = `
<nav class="navbar navbar-expand-lg">
<div class="container-fluid">
<div class="search-container">
<button class="button1" id="closebtn"> <span class="X"></span><span class="Y"></span></button>
<input id="customInputField" class="input" placeholder="Search skins...">
<div class="dropdown profile-container">
<img src="https://i.imgur.com/V4RclNb.png" alt="Profile" class="profile-image dropdown-toggle" id="profileDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<ul class="dropdown-menu custom-dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
<li><a class="dropdown-item" href="#" id="discord-login-btn" data-action="login">Login</a></li>
<li><a class="dropdown-item" href="#" id="discord-logout-btn" data-action="logout" style="display: none;">Logout</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" id="refreshSkins">Refresh Skins</a></li>
</ul>
</div>
</div>
<a href="#" id="shine1" class="btn-shine">Public Skins</a>
</div>
</nav>
<div class="multi-button">
<button id="publicSkinsTab">PUBLIC SKINS</button>
<button id="mySkinsTab">MY SKINS</button>
<button id="userPPP">hello</button>
<button id="favoritesTab">❤️</button>
</div>
<div class="offcanvas-body">
<a href="#" id="shine2" class="btn-shine">Public Skins</a>
<div id="skinsContainer" style="display: none;"></div>
<div id="mySkinsContainer" style="display: none;"></div>
<div id="favoritesContainer" style="display: none;"></div>
</div>
`;
// Add the drawer to the body
document.body.appendChild(drawer);
// Configure the Public Skins button to open the drawer
button.setAttribute('data-bs-toggle', 'offcanvas');
button.setAttribute('data-bs-target', '#publicSkinsDrawer');
// Ensure Bootstrap initializes the drawer correctly
//const drawerInstance = new bootstrap.Offcanvas(drawer);
// Check if the close button and drawer elements exist before proceeding
const closeButton = document.getElementById("closebtn");
// Only attach the event listener if both elements exist
if (closeButton && drawer) {
closeButton.addEventListener("click", function () {
const bsDrawer = bootstrap.Offcanvas.getInstance(drawer);
if (bsDrawer) {
bsDrawer.hide();
console.log('Drawer closed.');
} else {
console.log('Bootstrap Offcanvas instance not found for the drawer.');
}
});
} else {
if (!closeButton) {
console.log('Close button with ID "closebtn" not found.');
}
if (!drawer) {
console.log('Drawer element with ID "publicSkinsDrawer" not found.');
}
}
// Discord OAuth URL
const DISCORD_OAUTH_URL = "https://discord.com/api/oauth2/authorize?client_id=1266230711690596455&redirect_uri=https://snay.vercel.app/api/discord&response_type=code&scope=identify";
function updateButtons() {
const storedUsername = localStorage.getItem("discordUsername");
const storedToken = localStorage.getItem("discordToken");
const username = new URLSearchParams(window.location.search).get("username") || storedUsername;
const token = new URLSearchParams(window.location.search).get("token") || storedToken;
const loginButton = document.getElementById("discord-login-btn");
const logoutButton = document.getElementById("discord-logout-btn");
const profileImage = document.getElementById("profileDropdown"); // Profile image element
const userprofile = document.getElementById("userPPP");
if (username && token) {
loginButton.innerText = `Logged in as ${username}`;
loginButton.disabled = true;
userprofile.innerText = `${username}`;
loginButton.style.display = "none";
logoutButton.style.display = "block";
userprofile.style.display = "block";
if (!storedUsername || !storedToken) {
// Store username and token if not already stored
localStorage.setItem("discordUsername", username);
localStorage.setItem("discordToken", token);
}
// Fetch Discord user information
fetch('https://discord.com/api/users/@me', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch user info: ${response.status}`);
}
return response.json();
})
.then(userData => {
console.log("Fetched Discord user data:", userData);
// Update profile picture
const avatarURL = userData.avatar
? `https://cdn.discordapp.com/avatars/${userData.id}/${userData.avatar}.png`
: `https://cdn.discordapp.com/embed/avatars/${userData.discriminator % 5}.png`; // Default avatar
profileImage.src = avatarURL;
})
.catch(error => {
console.error("Error fetching Discord user info:", error);
profileImage.src = "https://i.imgur.com/V4RclNb.png"; // Fallback profile image
});
// Remove query parameters from URL
if (new URLSearchParams(window.location.search).has("username")) {
history.replaceState(null, null, window.location.pathname);
}
} else {
loginButton.style.display = "block";
loginButton.innerText = "Login with Discord";
loginButton.disabled = false;
logoutButton.style.display = "none";
userprofile.style.display = "none";
// Reset to default profile picture
if (profileImage) {
profileImage.src = "https://i.imgur.com/V4RclNb.png";
}
}
}
// Create Login Button Behavior
function createLoginButton() {
const loginButton = document.getElementById("discord-login-btn");
loginButton.addEventListener("click", (event) => {
event.preventDefault();
window.location.href = DISCORD_OAUTH_URL;
});
}
// Create Logout Button Behavior
function createLogoutButton() {
const logoutButton = document.getElementById("discord-logout-btn");
logoutButton.addEventListener("click", (event) => {
event.preventDefault();
localStorage.removeItem("discordUsername");
localStorage.removeItem("discordToken");
updateButtons();
});
}
// Initialize Buttons
if (document.readyState === "complete" || document.readyState === "interactive") {
createLoginButton();
createLogoutButton();
updateButtons();
} else {
document.addEventListener("DOMContentLoaded", () => {
createLoginButton();
createLogoutButton();
updateButtons();
});
}
button.setAttribute('data-bs-toggle', 'offcanvas');
button.setAttribute('data-bs-target', '#publicSkinsDrawer');
const skinsContainer = document.getElementById('skinsContainer');
const customInputField = document.getElementById('customInputField');
// Tooltip Initialization
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
setupTabSwitching();
// Refresh Skins Functionality
document.getElementById('refreshSkins').addEventListener('click', async () => {
window.showNotification("error", "Error", "Something went wrong.", 3000);
skinsContainer.innerHTML = `
<div class="dot-spinner">
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
<div class="dot-spinner__dot"></div>
</div>`;
try {
const images = await fetchImages();
renderImagesLazy(images, skinsContainer);
console.log("Skins refreshed successfully!");
} catch (error) {
console.log("Failed to refresh skins.");
}
});
try {
const images = await fetchImages();
renderImagesLazy(images, skinsContainer);
// Add search functionality
customInputField.addEventListener('input', () => {
const searchTerm = customInputField.value.toLowerCase();
const filteredImages = images.filter(image => image.name.toLowerCase().includes(searchTerm));
renderImagesLazy(filteredImages, skinsContainer);
});
} catch (error) {
skinsContainer.innerHTML = '<p style="color: white;">Failed to load skins. Please try again later.</p>';
}
}
main();
})();