Files
2025-10-05 00:58:08 -07:00

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