515 lines
15 KiB
Markdown
515 lines
15 KiB
Markdown
# FurryPlace Plugins
|
|
|
|
This directory contains plugins that extend the FurryPlace UI using the FurryPlace SDK.
|
|
|
|
## Quick Start
|
|
|
|
1. **Create a plugin file** in this directory (e.g., `my-plugin.js`)
|
|
2. **Refresh the page** - Plugins are automatically discovered and loaded!
|
|
3. **Use the SDK** to register buttons or add other functionality
|
|
|
|
> **✨ Auto-Loading:** Plugins are automatically discovered from the `frontend-backup/plugins/` directory. Any `.js` file you add here will be automatically loaded when the page loads. No need to manually edit HTML files!
|
|
|
|
## Example Plugins
|
|
|
|
### [example-button.js](./example-button.js)
|
|
Complete example showing how to:
|
|
- Add custom buttons with icons
|
|
- Handle click events
|
|
- Use conditional rendering
|
|
- Apply custom styling
|
|
- Open external links
|
|
|
|
### [example-user-state.js](./example-user-state.js)
|
|
Demonstrates user state access:
|
|
- Display user stats and information
|
|
- Show droplet counter
|
|
- Monitor paint charge status
|
|
- Display level progress
|
|
- Conditional rendering based on login status
|
|
- Refresh user data from server
|
|
|
|
### [example-info-modal.js](./example-info-modal.js)
|
|
Demonstrates info modal customization:
|
|
- Add custom sections to the info modal
|
|
- Display announcements and updates
|
|
- Show server statistics
|
|
- Add custom links and resources
|
|
- Display user-specific information
|
|
- Create interactive collapsible sections
|
|
- Add tips, tricks, and changelogs
|
|
|
|
## Captcha Plugins
|
|
|
|
FurryPlace includes built-in support for captcha verification at different points:
|
|
- **Login**: When logging into an existing account
|
|
- **Registration**: When creating a new account
|
|
- **Paint**: When painting pixels (can be configured to require every N paints)
|
|
- **Google OAuth**: Before initiating Google OAuth flow
|
|
|
|
### [captcha-turnstile.js](./captcha-turnstile.js)
|
|
Cloudflare Turnstile captcha integration:
|
|
- Free and privacy-friendly
|
|
- Lightweight and fast
|
|
- Automatic theme detection
|
|
- Configure per verification point
|
|
|
|
**Configuration:**
|
|
```javascript
|
|
window.FURRYPLACE_CAPTCHA_CONFIG = {
|
|
login: {
|
|
enabled: true,
|
|
siteKey: 'your-turnstile-site-key',
|
|
theme: 'auto', // 'light', 'dark', or 'auto'
|
|
},
|
|
register: {
|
|
enabled: true,
|
|
siteKey: 'your-turnstile-site-key',
|
|
theme: 'auto',
|
|
},
|
|
paint: {
|
|
enabled: true,
|
|
siteKey: 'your-turnstile-site-key',
|
|
theme: 'dark',
|
|
},
|
|
googleOAuth: {
|
|
enabled: true,
|
|
siteKey: 'your-turnstile-site-key',
|
|
theme: 'light',
|
|
}
|
|
};
|
|
```
|
|
|
|
### [captcha-hcaptcha.js](./captcha-hcaptcha.js)
|
|
hCaptcha integration:
|
|
- Privacy-focused alternative
|
|
- Customizable themes and sizes
|
|
- Supports compact mode
|
|
|
|
**Configuration:**
|
|
```javascript
|
|
window.FURRYPLACE_CAPTCHA_CONFIG = {
|
|
login: {
|
|
enabled: true,
|
|
siteKey: 'your-hcaptcha-site-key',
|
|
theme: 'light', // 'light' or 'dark'
|
|
size: 'normal', // 'normal', 'compact', or 'invisible'
|
|
},
|
|
register: {
|
|
enabled: true,
|
|
siteKey: 'your-hcaptcha-site-key',
|
|
theme: 'dark',
|
|
size: 'normal',
|
|
},
|
|
paint: {
|
|
enabled: true,
|
|
siteKey: 'your-hcaptcha-site-key',
|
|
theme: 'dark',
|
|
size: 'compact',
|
|
}
|
|
};
|
|
```
|
|
|
|
### [captcha-recaptcha.js](./captcha-recaptcha.js)
|
|
Google reCAPTCHA v2 and v3 integration:
|
|
- Support for both v2 (checkbox) and v3 (invisible)
|
|
- Per-action configuration for v3
|
|
- Customizable themes for v2
|
|
|
|
**Configuration:**
|
|
```javascript
|
|
window.FURRYPLACE_CAPTCHA_CONFIG = {
|
|
login: {
|
|
enabled: true,
|
|
siteKey: 'your-recaptcha-site-key',
|
|
version: 'v2', // 'v2' or 'v3'
|
|
theme: 'light', // v2 only
|
|
size: 'normal', // v2 only
|
|
action: 'login', // v3 only
|
|
},
|
|
paint: {
|
|
enabled: true,
|
|
siteKey: 'your-recaptcha-v3-site-key',
|
|
version: 'v3',
|
|
action: 'paint',
|
|
}
|
|
};
|
|
```
|
|
|
|
**Important:** Only load ONE captcha plugin at a time. Choose the one that matches your backend configuration.
|
|
|
|
## FurryPlace SDK API
|
|
|
|
### Button Registration
|
|
|
|
#### `window.FurryPlaceSDK.registerButton(config)`
|
|
|
|
Register a custom button in the UI.
|
|
|
|
**Configuration Object:**
|
|
|
|
```javascript
|
|
{
|
|
id: string, // Required: Unique identifier
|
|
title: string, // Required: Tooltip text
|
|
icon: string|HTMLElement, // Required: SVG string or DOM element
|
|
onClick: Function, // Required: (context, event) => {}
|
|
position: string, // Optional: 'top', 'bottom', 'before-leaderboard', 'after-leaderboard'
|
|
className: string, // Optional: Custom CSS classes (default: 'btn btn-square shadow-md')
|
|
wrapperClass: string, // Optional: Wrapper div CSS classes
|
|
condition: Function, // Optional: (context) => boolean
|
|
disabled: boolean // Optional: Whether button is disabled
|
|
}
|
|
```
|
|
|
|
**Positions:**
|
|
- `'top'` - First button in the container
|
|
- `'bottom'` - Last button in the container
|
|
- `'before-leaderboard'` - Before the leaderboard button
|
|
- `'after-leaderboard'` - After the leaderboard button (default)
|
|
|
|
**Context Object** (passed to `onClick` and `condition`):
|
|
|
|
```javascript
|
|
{
|
|
sdk: {
|
|
version: string, // SDK version
|
|
refreshUserState: Function // Function to refresh user state
|
|
},
|
|
user: {
|
|
isLoggedIn: boolean, // User login status
|
|
id: number|null, // User ID
|
|
name: string|null, // Display name
|
|
discord: string|null, // Discord username
|
|
country: string|null, // User's country
|
|
banned: boolean, // Is user banned
|
|
role: string|null, // User role (admin/moderator/user)
|
|
level: number, // Current level
|
|
pixelsPainted: number, // Total pixels painted
|
|
droplets: number, // Currency amount
|
|
picture: string|null, // Profile picture URL
|
|
equippedFlag: string|null, // Equipped country flag
|
|
allianceId: number|null, // Alliance ID
|
|
allianceRole: string|null, // Alliance role
|
|
charges: {
|
|
current: number, // Current paint charges
|
|
max: number, // Maximum charges
|
|
cooldownMs: number, // Cooldown in milliseconds
|
|
percentage: number // Charge percentage (0-100)
|
|
},
|
|
hasChargesToPaint: boolean, // Can paint (>= 1 charge)
|
|
needsPhoneVerification: boolean, // Needs phone verification
|
|
isTimedOut: boolean, // Currently timed out
|
|
timeoutUntil: string|null // Timeout expiry ISO string
|
|
},
|
|
map: {} // Map context (future use)
|
|
}
|
|
```
|
|
|
|
### User State Methods
|
|
|
|
#### `await FurryPlaceSDK.getUser()`
|
|
Get full user data (async). Returns user object or `null` if not logged in.
|
|
|
|
#### `await FurryPlaceSDK.refreshUser()`
|
|
Refresh user state from server and return updated data.
|
|
|
|
#### `FurryPlaceSDK.isLoggedIn()`
|
|
Returns `true` if user is logged in.
|
|
|
|
#### `FurryPlaceSDK.getDroplets()`
|
|
Get user's droplet count (currency).
|
|
|
|
#### `FurryPlaceSDK.getCharges()`
|
|
Get paint charge information:
|
|
```javascript
|
|
{
|
|
current: number, // Current charges
|
|
max: number, // Max capacity
|
|
cooldownMs: number, // Recharge cooldown
|
|
percentage: number // Fill percentage (0-100)
|
|
}
|
|
```
|
|
|
|
#### `FurryPlaceSDK.hasChargesToPaint()`
|
|
Returns `true` if user has at least 1 charge.
|
|
|
|
#### `FurryPlaceSDK.getLevel()`
|
|
Get user's current level.
|
|
|
|
#### `FurryPlaceSDK.getPixelsPainted()`
|
|
Get total pixels painted by user.
|
|
|
|
#### `FurryPlaceSDK.getAllianceId()`
|
|
Get user's alliance ID or `null`.
|
|
|
|
#### `FurryPlaceSDK.isInAlliance()`
|
|
Returns `true` if user is in an alliance.
|
|
|
|
### Info Modal Customization
|
|
|
|
#### `FurryPlaceSDK.addInfoSection(config)`
|
|
Add a custom section to the info modal (the one with rules and YouTube video).
|
|
|
|
**Configuration Object:**
|
|
```javascript
|
|
{
|
|
id: string, // Required: Unique identifier
|
|
title: string, // Optional: Section title
|
|
content: string|HTMLElement|Function, // Required: HTML string, DOM element, or function
|
|
position: string, // Optional: 'top', 'bottom', 'after-video'
|
|
className: string // Optional: Custom CSS classes for section
|
|
}
|
|
```
|
|
|
|
**Positions:**
|
|
- `'top'` - At the beginning of the modal
|
|
- `'bottom'` - At the end (before footer)
|
|
- `'after-video'` - Right after the YouTube video section
|
|
|
|
**Content Types:**
|
|
- **String**: HTML string that will be inserted
|
|
- **HTMLElement**: DOM element to append
|
|
- **Function**: Function that returns a string or HTMLElement
|
|
|
|
#### `FurryPlaceSDK.removeInfoSection(id)`
|
|
Remove a custom section from the info modal.
|
|
|
|
#### `FurryPlaceSDK.getInfoSections()`
|
|
Get list of all registered info sections.
|
|
|
|
### Captcha Methods
|
|
|
|
#### `FurryPlaceSDK.registerCaptchaCallback(verificationPoint, callback)`
|
|
Register a callback function that provides captcha tokens for a specific verification point.
|
|
|
|
**Parameters:**
|
|
- `verificationPoint`: One of `'login'`, `'register'`, `'googleOAuth'`, or `'paint'`
|
|
- `callback`: Async function that returns a captcha token string
|
|
|
|
**Example:**
|
|
```javascript
|
|
window.FurryPlaceSDK.registerCaptchaCallback('login', async () => {
|
|
// Show captcha UI and wait for user to complete it
|
|
const token = await showCaptchaWidget();
|
|
return token;
|
|
});
|
|
```
|
|
|
|
#### `await FurryPlaceSDK.requestCaptcha(verificationPoint)`
|
|
Request a captcha token for a specific verification point. This calls the registered callback.
|
|
|
|
**Returns:** Promise that resolves to a captcha token string or `null` if no callback is registered.
|
|
|
|
#### `FurryPlaceSDK.hasCaptchaCallback(verificationPoint)`
|
|
Check if a captcha callback is registered for a verification point.
|
|
|
|
**Returns:** `true` if callback is registered, `false` otherwise.
|
|
|
|
#### `FurryPlaceSDK.unregisterCaptchaCallback(verificationPoint)`
|
|
Remove a captcha callback for a verification point.
|
|
|
|
#### `FurryPlaceSDK.getCaptchaCallbacks()`
|
|
Get list of all verification points with registered callbacks.
|
|
|
|
**Returns:** Array of strings (verification point names).
|
|
|
|
### Other SDK Methods
|
|
|
|
- `FurryPlaceSDK.unregisterButton(id)` - Remove a registered button
|
|
- `FurryPlaceSDK.getButtons()` - Get list of all registered buttons
|
|
- `FurryPlaceSDK.getContext()` - Get current context object
|
|
- `FurryPlaceSDK.init()` - Manually initialize SDK (usually automatic)
|
|
|
|
## Example Usage
|
|
|
|
### Basic Button
|
|
|
|
```javascript
|
|
(function() {
|
|
'use strict';
|
|
|
|
function waitForSDK(callback) {
|
|
if (window.FurryPlaceSDK) {
|
|
callback();
|
|
} else {
|
|
setTimeout(() => waitForSDK(callback), 100);
|
|
}
|
|
}
|
|
|
|
waitForSDK(() => {
|
|
window.FurryPlaceSDK.registerButton({
|
|
id: 'my-custom-button',
|
|
title: 'My Button',
|
|
position: 'bottom',
|
|
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" class="size-5">
|
|
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/>
|
|
</svg>`,
|
|
onClick: (context) => {
|
|
alert('Button clicked!');
|
|
}
|
|
});
|
|
});
|
|
})();
|
|
```
|
|
|
|
### Accessing User State
|
|
|
|
```javascript
|
|
waitForSDK(() => {
|
|
// Synchronous methods
|
|
console.log('Logged in:', window.FurryPlaceSDK.isLoggedIn());
|
|
console.log('Droplets:', window.FurryPlaceSDK.getDroplets());
|
|
console.log('Level:', window.FurryPlaceSDK.getLevel());
|
|
|
|
// Async method for full user data
|
|
window.FurryPlaceSDK.getUser().then(user => {
|
|
if (user) {
|
|
console.log('User name:', user.name);
|
|
console.log('Alliance:', user.allianceId);
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
### Button with User State Condition
|
|
|
|
```javascript
|
|
// Only show button if user is logged in and has charges
|
|
window.FurryPlaceSDK.registerButton({
|
|
id: 'paint-helper',
|
|
title: 'Quick Paint',
|
|
position: 'top',
|
|
icon: `...`,
|
|
onClick: async (context) => {
|
|
const charges = context.user.charges;
|
|
alert(`You have ${charges.current}/${charges.max} charges`);
|
|
},
|
|
condition: (context) => {
|
|
return context.user?.isLoggedIn && context.user?.hasChargesToPaint;
|
|
}
|
|
});
|
|
```
|
|
|
|
### Charge Monitor Example
|
|
|
|
```javascript
|
|
window.FurryPlaceSDK.registerButton({
|
|
id: 'charge-monitor',
|
|
title: 'Charge Monitor',
|
|
position: 'bottom',
|
|
icon: `...`,
|
|
onClick: () => {
|
|
const charges = window.FurryPlaceSDK.getCharges();
|
|
alert(`
|
|
Current: ${charges.current}/${charges.max}
|
|
Percentage: ${charges.percentage.toFixed(1)}%
|
|
Can Paint: ${window.FurryPlaceSDK.hasChargesToPaint() ? 'Yes' : 'No'}
|
|
`);
|
|
},
|
|
condition: (context) => context.user?.isLoggedIn
|
|
});
|
|
```
|
|
|
|
### Info Modal Section Examples
|
|
|
|
#### Simple HTML Section
|
|
```javascript
|
|
window.FurryPlaceSDK.addInfoSection({
|
|
id: 'welcome-message',
|
|
title: 'Welcome!',
|
|
position: 'top',
|
|
content: '<p class="text-sm">Welcome to our server!</p>'
|
|
});
|
|
```
|
|
|
|
#### Dynamic Content with Function
|
|
```javascript
|
|
window.FurryPlaceSDK.addInfoSection({
|
|
id: 'user-stats',
|
|
title: 'Your Stats',
|
|
position: 'after-video',
|
|
content: () => {
|
|
const level = window.FurryPlaceSDK.getLevel();
|
|
const droplets = window.FurryPlaceSDK.getDroplets();
|
|
|
|
return `
|
|
<div class="text-sm">
|
|
<p>Level: ${level}</p>
|
|
<p>Droplets: ${droplets}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
```
|
|
|
|
#### Interactive Section with DOM Elements
|
|
```javascript
|
|
window.FurryPlaceSDK.addInfoSection({
|
|
id: 'interactive-section',
|
|
title: 'Custom Links',
|
|
position: 'bottom',
|
|
content: () => {
|
|
const div = document.createElement('div');
|
|
div.className = 'flex gap-2';
|
|
|
|
const button = document.createElement('button');
|
|
button.className = 'btn btn-sm btn-primary';
|
|
button.textContent = 'Click Me';
|
|
button.onclick = () => alert('Button clicked!');
|
|
|
|
div.appendChild(button);
|
|
return div;
|
|
}
|
|
});
|
|
```
|
|
|
|
## Finding SVG Icons
|
|
|
|
You can find Material Design icons at:
|
|
- https://fonts.google.com/icons
|
|
|
|
Copy the SVG code and use it as the `icon` parameter.
|
|
|
|
## Tips
|
|
|
|
1. **Wrap your plugin in an IIFE** to avoid polluting the global scope
|
|
2. **Wait for the SDK** to be available before registering buttons
|
|
3. **Use unique IDs** to avoid conflicts with other plugins
|
|
4. **Test button positions** to find the best placement for your use case
|
|
5. **Check the console** for SDK loading messages and errors
|
|
|
|
## How Auto-Loading Works
|
|
|
|
The SDK automatically:
|
|
1. Calls `/api/plugins` to get a list of all `.js` files in the `plugins/` directory
|
|
2. Dynamically loads each plugin by creating `<script>` tags
|
|
3. Reports loading progress in the browser console
|
|
|
|
You can check which plugins are loaded by looking at the console:
|
|
```
|
|
[FurryPlace SDK] Auto-discovering plugins...
|
|
[FurryPlace SDK] Found 2 plugin(s): ['example-button.js', 'my-plugin.js']
|
|
[FurryPlace SDK] Loaded plugin: example-button.js
|
|
[FurryPlace SDK] Loaded plugin: my-plugin.js
|
|
```
|
|
|
|
> **Note:** If you need to disable a plugin, either delete it or rename it to something other than `.js` (like `.js.disabled`)
|
|
|
|
## Troubleshooting
|
|
|
|
**Button not appearing?**
|
|
- Check the browser console for errors
|
|
- Verify the SDK is loaded: `console.log(window.FurryPlaceSDK)`
|
|
- Ensure your button ID is unique
|
|
- Try different positions
|
|
|
|
**Button appearing in wrong place?**
|
|
- Try different `position` values
|
|
- Check if the button container has changed in the UI
|
|
|
|
**Icon not showing?**
|
|
- Verify your SVG has `viewBox` and `fill="currentColor"`
|
|
- Add `class="size-5"` to the SVG element
|
|
- Check for syntax errors in the SVG string
|