Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- android/app/release/baselineProfiles/0/app-release.dm +0 -0
- android/app/release/baselineProfiles/1/app-release.dm +0 -0
- android/app/release/output-metadata.json +37 -0
- client/components/AuthModal.tsx +2 -1
- client/config.ts +30 -0
- client/getBookmarkPreview.tsx +2 -1
- client/hooks/useBackups.ts +5 -5
- client/multiplayerAssetStore.tsx +2 -2
- client/pages/Lobby.tsx +2 -1
- client/pages/RoomPage.tsx +2 -2
- public/color_rm.html +1 -0
- public/scripts/ColorRmApp.js +27 -2
android/app/release/baselineProfiles/0/app-release.dm
ADDED
|
Binary file (3.06 kB). View file
|
|
|
android/app/release/baselineProfiles/1/app-release.dm
ADDED
|
Binary file (3.01 kB). View file
|
|
|
android/app/release/output-metadata.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 3,
|
| 3 |
+
"artifactType": {
|
| 4 |
+
"type": "APK",
|
| 5 |
+
"kind": "Directory"
|
| 6 |
+
},
|
| 7 |
+
"applicationId": "shubham.akshit.tldraw",
|
| 8 |
+
"variantName": "release",
|
| 9 |
+
"elements": [
|
| 10 |
+
{
|
| 11 |
+
"type": "SINGLE",
|
| 12 |
+
"filters": [],
|
| 13 |
+
"attributes": [],
|
| 14 |
+
"versionCode": 1,
|
| 15 |
+
"versionName": "1.0",
|
| 16 |
+
"outputFile": "app-release.apk"
|
| 17 |
+
}
|
| 18 |
+
],
|
| 19 |
+
"elementType": "File",
|
| 20 |
+
"baselineProfiles": [
|
| 21 |
+
{
|
| 22 |
+
"minApi": 28,
|
| 23 |
+
"maxApi": 30,
|
| 24 |
+
"baselineProfiles": [
|
| 25 |
+
"baselineProfiles/1/app-release.dm"
|
| 26 |
+
]
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"minApi": 31,
|
| 30 |
+
"maxApi": 2147483647,
|
| 31 |
+
"baselineProfiles": [
|
| 32 |
+
"baselineProfiles/0/app-release.dm"
|
| 33 |
+
]
|
| 34 |
+
}
|
| 35 |
+
],
|
| 36 |
+
"minSdkVersionForDexing": 24
|
| 37 |
+
}
|
client/components/AuthModal.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { useState } from 'react'
|
| 2 |
import { useAuth } from '../hooks/useAuth'
|
| 3 |
import { colors } from '../constants/theme'
|
|
|
|
| 4 |
|
| 5 |
interface AuthModalProps {
|
| 6 |
isOpen: boolean
|
|
@@ -25,7 +26,7 @@ export function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|
| 25 |
try {
|
| 26 |
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register'
|
| 27 |
|
| 28 |
-
const res = await fetch(endpoint, {
|
| 29 |
method: 'POST',
|
| 30 |
headers: { 'Content-Type': 'application/json' },
|
| 31 |
body: JSON.stringify({ username, password })
|
|
|
|
| 1 |
import { useState } from 'react'
|
| 2 |
import { useAuth } from '../hooks/useAuth'
|
| 3 |
import { colors } from '../constants/theme'
|
| 4 |
+
import { apiUrl } from '../config'
|
| 5 |
|
| 6 |
interface AuthModalProps {
|
| 7 |
isOpen: boolean
|
|
|
|
| 26 |
try {
|
| 27 |
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register'
|
| 28 |
|
| 29 |
+
const res = await fetch(apiUrl(endpoint), {
|
| 30 |
method: 'POST',
|
| 31 |
headers: { 'Content-Type': 'application/json' },
|
| 32 |
body: JSON.stringify({ username, password })
|
client/config.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
| 1 |
// --- SINGLE SOURCE OF TRUTH ---
|
| 2 |
const VITE_PREVIEW_URL = import.meta.env.VITE_PREVIEW_URL
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const VERSION = "1.0.5-" + Date.now();
|
| 5 |
console.log(`[Config] Version: ${VERSION}`);
|
| 6 |
console.log(`[Config] window.location:`, {
|
|
@@ -11,13 +16,38 @@ console.log(`[Config] window.location:`, {
|
|
| 11 |
|
| 12 |
let origin = window.location.origin
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
if (VITE_PREVIEW_URL) {
|
| 15 |
console.log(`[Config] Using VITE_PREVIEW_URL: ${VITE_PREVIEW_URL}`);
|
| 16 |
origin = VITE_PREVIEW_URL.replace(/\/$/, '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
// Force WSS if we are on HTTPS
|
| 20 |
export const SERVER_URL = origin
|
| 21 |
export const WS_URL = origin.replace(/^http/, 'ws')
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
console.log(`[Config] FINAL -> SERVER_URL: ${SERVER_URL}, WS_URL: ${WS_URL}`);
|
|
|
|
| 1 |
// --- SINGLE SOURCE OF TRUTH ---
|
| 2 |
const VITE_PREVIEW_URL = import.meta.env.VITE_PREVIEW_URL
|
| 3 |
|
| 4 |
+
const BACKENDS = {
|
| 5 |
+
hf: 'https://jaimodiji-my-multiplayer-app.hf.space',
|
| 6 |
+
cloudflare: 'https://multiplayer-template.bossemail.workers.dev'
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
const VERSION = "1.0.5-" + Date.now();
|
| 10 |
console.log(`[Config] Version: ${VERSION}`);
|
| 11 |
console.log(`[Config] window.location:`, {
|
|
|
|
| 16 |
|
| 17 |
let origin = window.location.origin
|
| 18 |
|
| 19 |
+
// Helper to detect if we are in a "remote" mode (served from a server, not bundled)
|
| 20 |
+
const isRemoteMode = () => {
|
| 21 |
+
const host = window.location.host;
|
| 22 |
+
return host.includes('hf.space') ||
|
| 23 |
+
host.includes('workers.dev') ||
|
| 24 |
+
host.includes('duckdns.org') ||
|
| 25 |
+
host.includes('192.168.');
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
// Helper to detect Capacitor
|
| 29 |
+
const isCapacitor = () => {
|
| 30 |
+
return !!(window as any).Capacitor;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
if (VITE_PREVIEW_URL) {
|
| 34 |
console.log(`[Config] Using VITE_PREVIEW_URL: ${VITE_PREVIEW_URL}`);
|
| 35 |
origin = VITE_PREVIEW_URL.replace(/\/$/, '')
|
| 36 |
+
} else if (isCapacitor() && !isRemoteMode()) {
|
| 37 |
+
// We are likely in bundled mode on a device
|
| 38 |
+
const preferredBackend = localStorage.getItem('color_rm_backend') || 'cloudflare';
|
| 39 |
+
origin = (BACKENDS as any)[preferredBackend] || BACKENDS.cloudflare;
|
| 40 |
+
console.log(`[Config] Capacitor bundled mode detected. Using backend: ${origin}`);
|
| 41 |
}
|
| 42 |
|
| 43 |
// Force WSS if we are on HTTPS
|
| 44 |
export const SERVER_URL = origin
|
| 45 |
export const WS_URL = origin.replace(/^http/, 'ws')
|
| 46 |
|
| 47 |
+
export const apiUrl = (path: string) => {
|
| 48 |
+
// Ensure path starts with /
|
| 49 |
+
const normalizedPath = path.startsWith('/') ? path : '/' + path;
|
| 50 |
+
return SERVER_URL + normalizedPath;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
console.log(`[Config] FINAL -> SERVER_URL: ${SERVER_URL}, WS_URL: ${WS_URL}`);
|
client/getBookmarkPreview.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { AssetRecordType, TLAsset, TLBookmarkAsset, getHashForString } from 'tldraw'
|
|
|
|
| 2 |
|
| 3 |
// How does our server handle bookmark unfurling?
|
| 4 |
export async function getBookmarkPreview({ url }: { url: string }): Promise<TLAsset> {
|
|
@@ -19,7 +20,7 @@ export async function getBookmarkPreview({ url }: { url: string }): Promise<TLAs
|
|
| 19 |
|
| 20 |
try {
|
| 21 |
// try to fetch the preview data from the server
|
| 22 |
-
const response = await fetch(`/api/unfurl?url=${encodeURIComponent(url)}`)
|
| 23 |
const data: any = await response.json()
|
| 24 |
|
| 25 |
// fill in our asset with whatever info we found
|
|
|
|
| 1 |
import { AssetRecordType, TLAsset, TLBookmarkAsset, getHashForString } from 'tldraw'
|
| 2 |
+
import { apiUrl } from './config'
|
| 3 |
|
| 4 |
// How does our server handle bookmark unfurling?
|
| 5 |
export async function getBookmarkPreview({ url }: { url: string }): Promise<TLAsset> {
|
|
|
|
| 20 |
|
| 21 |
try {
|
| 22 |
// try to fetch the preview data from the server
|
| 23 |
+
const response = await fetch(apiUrl(`/api/unfurl?url=${encodeURIComponent(url)}`))
|
| 24 |
const data: any = await response.json()
|
| 25 |
|
| 26 |
// fill in our asset with whatever info we found
|
client/hooks/useBackups.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { useState, useCallback } from 'react'
|
| 2 |
import { useAuth } from './useAuth'
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
export interface BackupItem {
|
| 6 |
key: string
|
|
@@ -22,7 +22,7 @@ export function useBackups() {
|
|
| 22 |
setIsLoading(true)
|
| 23 |
setError(null)
|
| 24 |
try {
|
| 25 |
-
const res = await fetch(
|
| 26 |
headers: { 'Authorization': `Bearer ${token}` }
|
| 27 |
})
|
| 28 |
if (!res.ok) throw new Error('Failed to fetch backups')
|
|
@@ -39,7 +39,7 @@ export function useBackups() {
|
|
| 39 |
if (!isAuthenticated || !token) throw new Error('Not authenticated')
|
| 40 |
|
| 41 |
try {
|
| 42 |
-
const res = await fetch(
|
| 43 |
method: 'POST',
|
| 44 |
headers: {
|
| 45 |
'Content-Type': 'application/json',
|
|
@@ -62,7 +62,7 @@ export function useBackups() {
|
|
| 62 |
|
| 63 |
try {
|
| 64 |
const encodedKey = encodeURIComponent(key)
|
| 65 |
-
const res = await fetch(`
|
| 66 |
method: 'DELETE',
|
| 67 |
headers: { 'Authorization': `Bearer ${token}` }
|
| 68 |
})
|
|
@@ -85,7 +85,7 @@ export function useBackups() {
|
|
| 85 |
// The server expects encoded key in param: /api/backup/:key
|
| 86 |
const encodedKey = encodeURIComponent(key)
|
| 87 |
|
| 88 |
-
const res = await fetch(`
|
| 89 |
headers: { 'Authorization': `Bearer ${token}` }
|
| 90 |
})
|
| 91 |
|
|
|
|
| 1 |
import { useState, useCallback } from 'react'
|
| 2 |
import { useAuth } from './useAuth'
|
| 3 |
+
import { apiUrl } from '../config'
|
| 4 |
|
| 5 |
export interface BackupItem {
|
| 6 |
key: string
|
|
|
|
| 22 |
setIsLoading(true)
|
| 23 |
setError(null)
|
| 24 |
try {
|
| 25 |
+
const res = await fetch(apiUrl('/api/backups'), {
|
| 26 |
headers: { 'Authorization': `Bearer ${token}` }
|
| 27 |
})
|
| 28 |
if (!res.ok) throw new Error('Failed to fetch backups')
|
|
|
|
| 39 |
if (!isAuthenticated || !token) throw new Error('Not authenticated')
|
| 40 |
|
| 41 |
try {
|
| 42 |
+
const res = await fetch(apiUrl('/api/backup'), {
|
| 43 |
method: 'POST',
|
| 44 |
headers: {
|
| 45 |
'Content-Type': 'application/json',
|
|
|
|
| 62 |
|
| 63 |
try {
|
| 64 |
const encodedKey = encodeURIComponent(key)
|
| 65 |
+
const res = await fetch(apiUrl(`/api/backup/${encodedKey}`), {
|
| 66 |
method: 'DELETE',
|
| 67 |
headers: { 'Authorization': `Bearer ${token}` }
|
| 68 |
})
|
|
|
|
| 85 |
// The server expects encoded key in param: /api/backup/:key
|
| 86 |
const encodedKey = encodeURIComponent(key)
|
| 87 |
|
| 88 |
+
const res = await fetch(apiUrl(`/api/backup/${encodedKey}`), {
|
| 89 |
headers: { 'Authorization': `Bearer ${token}` }
|
| 90 |
})
|
| 91 |
|
client/multiplayerAssetStore.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { TLAssetStore, uniqueId } from 'tldraw'
|
| 2 |
// 1. IMPORT CONFIG
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
export const multiplayerAssetStore: TLAssetStore = {
|
| 6 |
async upload(_asset, file) {
|
|
@@ -8,7 +8,7 @@ export const multiplayerAssetStore: TLAssetStore = {
|
|
| 8 |
const objectName = `${id}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, '-')
|
| 9 |
|
| 10 |
// 2. USE CONFIG
|
| 11 |
-
const url = `
|
| 12 |
|
| 13 |
const response = await fetch(url, {
|
| 14 |
method: 'POST',
|
|
|
|
| 1 |
import { TLAssetStore, uniqueId } from 'tldraw'
|
| 2 |
// 1. IMPORT CONFIG
|
| 3 |
+
import { apiUrl } from './config'
|
| 4 |
|
| 5 |
export const multiplayerAssetStore: TLAssetStore = {
|
| 6 |
async upload(_asset, file) {
|
|
|
|
| 8 |
const objectName = `${id}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, '-')
|
| 9 |
|
| 10 |
// 2. USE CONFIG
|
| 11 |
+
const url = apiUrl(`/api/uploads/${objectName}`)
|
| 12 |
|
| 13 |
const response = await fetch(url, {
|
| 14 |
method: 'POST',
|
client/pages/Lobby.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
|
| 2 |
import { useNavigate } from 'react-router-dom'
|
| 3 |
import { uniqueId } from 'tldraw'
|
| 4 |
|
|
|
|
| 5 |
import { generateBoardName } from '../utils/nameGenerator'
|
| 6 |
import { saveRoom } from './storageUtils'
|
| 7 |
import { useAuth } from '../hooks/useAuth'
|
|
@@ -44,7 +45,7 @@ export function Lobby() {
|
|
| 44 |
|
| 45 |
useEffect(() => {
|
| 46 |
if (isAuthenticated && token) {
|
| 47 |
-
fetch('/api/color_rm/registry', {
|
| 48 |
headers: {
|
| 49 |
'Authorization': `Bearer ${token}`
|
| 50 |
}
|
|
|
|
| 2 |
import { useNavigate } from 'react-router-dom'
|
| 3 |
import { uniqueId } from 'tldraw'
|
| 4 |
|
| 5 |
+
import { apiUrl } from '../config'
|
| 6 |
import { generateBoardName } from '../utils/nameGenerator'
|
| 7 |
import { saveRoom } from './storageUtils'
|
| 8 |
import { useAuth } from '../hooks/useAuth'
|
|
|
|
| 45 |
|
| 46 |
useEffect(() => {
|
| 47 |
if (isAuthenticated && token) {
|
| 48 |
+
fetch(apiUrl('/api/color_rm/registry'), {
|
| 49 |
headers: {
|
| 50 |
'Authorization': `Bearer ${token}`
|
| 51 |
}
|
client/pages/RoomPage.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
| 18 |
import { useSync } from '@tldraw/sync'
|
| 19 |
import { getBookmarkPreview } from '../getBookmarkPreview'
|
| 20 |
import { multiplayerAssetStore } from '../multiplayerAssetStore'
|
| 21 |
-
import { WS_URL, SERVER_URL } from '../config'
|
| 22 |
import { saveRoom } from './storageUtils'
|
| 23 |
import { StatePersistence } from '../components/StatePersistence'
|
| 24 |
import { NavigationDock } from '../components/NavigationDock'
|
|
@@ -150,7 +150,7 @@ export function RoomPage() {
|
|
| 150 |
|
| 151 |
useEffect(() => {
|
| 152 |
saveRoom(roomId)
|
| 153 |
-
const metaUrl = `
|
| 154 |
console.log(`[Room] Fetching metadata from: ${metaUrl}`);
|
| 155 |
|
| 156 |
fetch(metaUrl)
|
|
|
|
| 18 |
import { useSync } from '@tldraw/sync'
|
| 19 |
import { getBookmarkPreview } from '../getBookmarkPreview'
|
| 20 |
import { multiplayerAssetStore } from '../multiplayerAssetStore'
|
| 21 |
+
import { WS_URL, SERVER_URL, apiUrl } from '../config'
|
| 22 |
import { saveRoom } from './storageUtils'
|
| 23 |
import { StatePersistence } from '../components/StatePersistence'
|
| 24 |
import { NavigationDock } from '../components/NavigationDock'
|
|
|
|
| 150 |
|
| 151 |
useEffect(() => {
|
| 152 |
saveRoom(roomId)
|
| 153 |
+
const metaUrl = apiUrl(`/api/meta/${roomId}`);
|
| 154 |
console.log(`[Room] Fetching metadata from: ${metaUrl}`);
|
| 155 |
|
| 156 |
fetch(metaUrl)
|
public/color_rm.html
CHANGED
|
@@ -384,6 +384,7 @@
|
|
| 384 |
</div>
|
| 385 |
<div class="picker-body">
|
| 386 |
<div id="iroWheel"></div>
|
|
|
|
| 387 |
<button id="pickerActionBtn" class="btn btn-primary" style="width:100%; margin-top:10px;">Set</button>
|
| 388 |
<button id="pickerNoneBtn" class="btn" style="width:100%; margin-top:5px; display:none">Transparent</button>
|
| 389 |
</div>
|
|
|
|
| 384 |
</div>
|
| 385 |
<div class="picker-body">
|
| 386 |
<div id="iroWheel"></div>
|
| 387 |
+
<div id="pickerSwatches" style="display:flex; gap:6px; margin-top:10px; flex-wrap:wrap; justify-content:center; width:100%;"></div>
|
| 388 |
<button id="pickerActionBtn" class="btn btn-primary" style="width:100%; margin-top:10px;">Set</button>
|
| 389 |
<button id="pickerNoneBtn" class="btn" style="width:100%; margin-top:5px; display:none">Transparent</button>
|
| 390 |
</div>
|
public/scripts/ColorRmApp.js
CHANGED
|
@@ -13,7 +13,8 @@ export class ColorRmApp {
|
|
| 13 |
|
| 14 |
this.state = {
|
| 15 |
sessionId: null, images: [], idx: 0,
|
| 16 |
-
colors: [],
|
|
|
|
| 17 |
penColor: '#ef4444', penSize: 3, eraserSize: 20, eraserType: 'stroke',
|
| 18 |
textSize: 40,
|
| 19 |
shapeType: 'rectangle', shapeBorder: '#3b82f6', shapeFill: 'transparent', shapeWidth: 3,
|
|
@@ -534,8 +535,15 @@ export class ColorRmApp {
|
|
| 534 |
const pickerAction = this.getElement('pickerActionBtn');
|
| 535 |
if(pickerAction) {
|
| 536 |
pickerAction.onclick = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
if(this.state.pickerMode==='remove') {
|
| 538 |
-
const hex = this.iroP.color.hexString;
|
| 539 |
const i = parseInt(hex.slice(1), 16);
|
| 540 |
this.state.colors.push({hex, lab:this.rgbToLab((i>>16)&255,(i>>8)&255,i&255)});
|
| 541 |
this.renderSwatches();
|
|
@@ -1231,6 +1239,7 @@ export class ColorRmApp {
|
|
| 1231 |
this.state.pickerMode=m;
|
| 1232 |
const pb = this.getElement('pickerNoneBtn');
|
| 1233 |
if(pb) pb.style.display = (m==='shapeFill'||m==='selectionFill') ? 'block' : 'none';
|
|
|
|
| 1234 |
const fp = this.getElement('floatingPicker');
|
| 1235 |
if(fp) fp.style.display='flex';
|
| 1236 |
}
|
|
@@ -1486,6 +1495,22 @@ export class ColorRmApp {
|
|
| 1486 |
});
|
| 1487 |
}
|
| 1488 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
switchSideTab(tab) {
|
| 1490 |
this.state.activeSideTab = tab;
|
| 1491 |
const tabs = ['tools', 'pages', 'box', 'debug'];
|
|
|
|
| 13 |
|
| 14 |
this.state = {
|
| 15 |
sessionId: null, images: [], idx: 0,
|
| 16 |
+
colors: [], customSwatches: JSON.parse(localStorage.getItem('crm_custom_colors') || '[]'),
|
| 17 |
+
strict: 15, tool: 'none', bg: 'transparent',
|
| 18 |
penColor: '#ef4444', penSize: 3, eraserSize: 20, eraserType: 'stroke',
|
| 19 |
textSize: 40,
|
| 20 |
shapeType: 'rectangle', shapeBorder: '#3b82f6', shapeFill: 'transparent', shapeWidth: 3,
|
|
|
|
| 535 |
const pickerAction = this.getElement('pickerActionBtn');
|
| 536 |
if(pickerAction) {
|
| 537 |
pickerAction.onclick = () => {
|
| 538 |
+
const hex = this.iroP.color.hexString;
|
| 539 |
+
|
| 540 |
+
// Save to custom swatches history (max 14)
|
| 541 |
+
this.state.customSwatches = this.state.customSwatches.filter(c => c !== hex);
|
| 542 |
+
this.state.customSwatches.unshift(hex);
|
| 543 |
+
if(this.state.customSwatches.length > 14) this.state.customSwatches.pop();
|
| 544 |
+
localStorage.setItem('crm_custom_colors', JSON.stringify(this.state.customSwatches));
|
| 545 |
+
|
| 546 |
if(this.state.pickerMode==='remove') {
|
|
|
|
| 547 |
const i = parseInt(hex.slice(1), 16);
|
| 548 |
this.state.colors.push({hex, lab:this.rgbToLab((i>>16)&255,(i>>8)&255,i&255)});
|
| 549 |
this.renderSwatches();
|
|
|
|
| 1239 |
this.state.pickerMode=m;
|
| 1240 |
const pb = this.getElement('pickerNoneBtn');
|
| 1241 |
if(pb) pb.style.display = (m==='shapeFill'||m==='selectionFill') ? 'block' : 'none';
|
| 1242 |
+
this.renderPickerSwatches();
|
| 1243 |
const fp = this.getElement('floatingPicker');
|
| 1244 |
if(fp) fp.style.display='flex';
|
| 1245 |
}
|
|
|
|
| 1495 |
});
|
| 1496 |
}
|
| 1497 |
|
| 1498 |
+
renderPickerSwatches() {
|
| 1499 |
+
const c = this.getElement('pickerSwatches');
|
| 1500 |
+
if (!c) return;
|
| 1501 |
+
c.innerHTML = '';
|
| 1502 |
+
this.state.customSwatches.forEach(color => {
|
| 1503 |
+
const d = document.createElement('div');
|
| 1504 |
+
d.style.width = '24px'; d.style.height = '24px';
|
| 1505 |
+
d.style.borderRadius = '4px'; d.style.border = '1px solid #333';
|
| 1506 |
+
d.style.cursor = 'pointer'; d.style.background = color;
|
| 1507 |
+
d.onclick = () => {
|
| 1508 |
+
if (this.iroP) this.iroP.color.set(color);
|
| 1509 |
+
};
|
| 1510 |
+
c.appendChild(d);
|
| 1511 |
+
});
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
switchSideTab(tab) {
|
| 1515 |
this.state.activeSideTab = tab;
|
| 1516 |
const tabs = ['tools', 'pages', 'box', 'debug'];
|