Skip to content

Commit 37a1855

Browse files
committed
1. Fixed menu y calc issues. 2. Reload preference whenever l2d may have been changed. 3. Default l2d guaranteed to be loaded when l2d not set. 4. Auto-binding voice id.
1 parent 41d6d80 commit 37a1855

6 files changed

Lines changed: 266 additions & 91 deletions

File tree

main_server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,26 @@ async def get_current_live2d_model(catgirl_name: str = ""):
817817
except Exception as e:
818818
logger.warning(f"获取模型信息失败: {e}")
819819

820+
# 回退机制:如果没有找到模型,使用默认的mao_pro
821+
if not live2d_model_name or not model_info:
822+
logger.info(f"猫娘 {catgirl_name} 未设置Live2D模型,回退到默认模型 mao_pro")
823+
live2d_model_name = 'mao_pro'
824+
try:
825+
# 查找mao_pro模型
826+
model_dir, url_prefix = find_model_directory('mao_pro')
827+
if os.path.exists(model_dir):
828+
model_files = [f for f in os.listdir(model_dir) if f.endswith('.model3.json')]
829+
if model_files:
830+
model_file = model_files[0]
831+
model_path = f'{url_prefix}/mao_pro/{model_file}'
832+
model_info = {
833+
'name': 'mao_pro',
834+
'path': model_path,
835+
'is_fallback': True # 标记这是回退模型
836+
}
837+
except Exception as e:
838+
logger.error(f"获取默认模型mao_pro失败: {e}")
839+
820840
return JSONResponse(content={
821841
'success': True,
822842
'catgirl_name': catgirl_name,
@@ -1459,6 +1479,12 @@ async def delete_catgirl(name: str):
14591479
characters = _config_manager.load_characters()
14601480
if name not in characters.get('猫娘', {}):
14611481
return JSONResponse({'success': False, 'error': '猫娘不存在'}, status_code=404)
1482+
1483+
# 检查是否是当前正在使用的猫娘
1484+
current_catgirl = characters.get('当前猫娘', '')
1485+
if name == current_catgirl:
1486+
return JSONResponse({'success': False, 'error': '不能删除当前正在使用的猫娘!请先切换到其他猫娘后再删除。'}, status_code=400)
1487+
14621488
del characters['猫娘'][name]
14631489
_config_manager.save_characters(characters)
14641490
# 自动重新加载配置

static/app.js

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2690,11 +2690,6 @@ function init_app(){
26902690
return;
26912691
}
26922692

2693-
if (newCatgirl === lanlan_config.lanlan_name) {
2694-
console.log('[猫娘切换] ℹ️ 新猫娘与当前相同,无需切换');
2695-
return;
2696-
}
2697-
26982693
console.log('[猫娘切换] 🚀 开始切换,从', lanlan_config.lanlan_name, '切换到', newCatgirl);
26992694

27002695
// 显示切换提示
@@ -2758,6 +2753,11 @@ function init_app(){
27582753
if (modelData.success && modelData.model_name && modelData.model_info) {
27592754
console.log('[猫娘切换] 检测到新猫娘的 Live2D 模型:', modelData.model_name, '路径:', modelData.model_info.path);
27602755

2756+
// 如果是回退模型,显示提示
2757+
if (modelData.model_info.is_fallback) {
2758+
console.log('[猫娘切换] ⚠️ 新猫娘未设置Live2D模型,使用默认模型 mao_pro');
2759+
}
2760+
27612761
// 检查 live2dManager 是否存在并已初始化
27622762
if (!window.live2dManager) {
27632763
console.error('[猫娘切换] live2dManager 不存在,无法重新加载模型');
@@ -2813,7 +2813,56 @@ function init_app(){
28132813
}
28142814
}
28152815
} else {
2816-
console.warn('[猫娘切换] 无法获取新猫娘的 Live2D 模型信息:', modelData);
2816+
console.warn('[猫娘切换] 无法获取新猫娘的 Live2D 模型信息,尝试加载默认模型 mao_pro:', modelData);
2817+
2818+
// 前端回退机制:如果后端没有返回有效的模型信息,尝试直接加载mao_pro
2819+
try {
2820+
console.log('[猫娘切换] 尝试回退到默认模型 mao_pro');
2821+
2822+
if (window.live2dManager && window.live2dManager.pixi_app) {
2823+
// 查找mao_pro模型
2824+
const modelsResponse = await fetch('/api/live2d/models');
2825+
if (modelsResponse.ok) {
2826+
const models = await modelsResponse.json();
2827+
const maoProModel = models.find(m => m.name === 'mao_pro');
2828+
2829+
if (maoProModel) {
2830+
console.log('[猫娘切换] 找到默认模型 mao_pro,路径:', maoProModel.path);
2831+
2832+
// 获取模型配置
2833+
const modelConfigRes = await fetch(maoProModel.path);
2834+
if (modelConfigRes.ok) {
2835+
const modelConfig = await modelConfigRes.json();
2836+
modelConfig.url = maoProModel.path;
2837+
2838+
// 加载默认模型
2839+
await window.live2dManager.loadModel(modelConfig, {
2840+
isMobile: window.innerWidth <= 768
2841+
});
2842+
2843+
// 更新全局引用
2844+
if (window.LanLan1) {
2845+
window.LanLan1.live2dModel = window.live2dManager.getCurrentModel();
2846+
window.LanLan1.currentModel = window.live2dManager.getCurrentModel();
2847+
window.LanLan1.emotionMapping = window.live2dManager.getEmotionMapping();
2848+
}
2849+
2850+
console.log('[猫娘切换] 已成功回退到默认模型 mao_pro');
2851+
} else {
2852+
console.error('[猫娘切换] 无法获取默认模型配置,状态:', modelConfigRes.status);
2853+
}
2854+
} else {
2855+
console.error('[猫娘切换] 未找到默认模型 mao_pro');
2856+
}
2857+
} else {
2858+
console.error('[猫娘切换] 无法获取模型列表');
2859+
}
2860+
} else {
2861+
console.error('[猫娘切换] live2dManager 未初始化,无法加载默认模型');
2862+
}
2863+
} catch (fallbackError) {
2864+
console.error('[猫娘切换] 回退到默认模型失败:', fallbackError);
2865+
}
28172866
}
28182867
showStatusToast(`已切换到 ${newCatgirl}`, 3000);
28192868
} catch (error) {
@@ -2920,3 +2969,13 @@ window.addEventListener("load", () => {
29202969
}, 1000);
29212970
});
29222971

2972+
// 监听voice_id更新消息
2973+
window.addEventListener('message', function(event) {
2974+
if (event.data.type === 'voice_id_updated') {
2975+
console.log('[Voice Clone] 收到voice_id更新消息:', event.data.voice_id);
2976+
if (typeof window.showStatusToast === 'function' && typeof lanlan_config !== 'undefined' && lanlan_config.lanlan_name) {
2977+
window.showStatusToast(`${lanlan_config.lanlan_name}的语音已更新`, 3000);
2978+
}
2979+
}
2980+
});
2981+

static/live2d.js

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,27 @@ class Live2DManager {
20992099
finalUrl = `${item.urlBase}?lanlan_name=${encodeURIComponent(lanlanName)}`;
21002100
// Live2D设置页直接跳转
21012101
window.location.href = finalUrl;
2102+
} else if (item.id === 'voice-clone' && item.url) {
2103+
// 声音克隆页面也需要传递 lanlan_name
2104+
const lanlanName = (window.lanlan_config && window.lanlan_config.lanlan_name) || '';
2105+
finalUrl = `${item.url}?lanlan_name=${encodeURIComponent(lanlanName)}`;
2106+
2107+
// 检查是否已有该URL的窗口打开
2108+
if (this._openSettingsWindows[finalUrl]) {
2109+
const existingWindow = this._openSettingsWindows[finalUrl];
2110+
if (existingWindow && !existingWindow.closed) {
2111+
existingWindow.focus();
2112+
return;
2113+
} else {
2114+
delete this._openSettingsWindows[finalUrl];
2115+
}
2116+
}
2117+
2118+
// 打开新窗口并保存引用
2119+
const newWindow = window.open(finalUrl, '_blank', 'width=1000,height=800,menubar=no,toolbar=no,location=no,status=no');
2120+
if (newWindow) {
2121+
this._openSettingsWindows[finalUrl] = newWindow;
2122+
}
21022123
} else {
21032124
// 其他页面弹出新窗口,但检查是否已打开
21042125
// 检查是否已有该URL的窗口打开
@@ -2170,10 +2191,17 @@ class Live2DManager {
21702191
popup.style.transform = 'translateX(-10px)';
21712192
setTimeout(() => {
21722193
popup.style.display = 'none';
2173-
// 重置位置
2194+
// 重置位置和样式
21742195
popup.style.left = '100%';
21752196
popup.style.right = 'auto';
21762197
popup.style.top = '0';
2198+
popup.style.marginLeft = '8px';
2199+
popup.style.marginRight = '0';
2200+
// 重置高度限制,确保下次打开时状态一致
2201+
if (buttonId === 'settings' || buttonId === 'agent') {
2202+
popup.style.maxHeight = '200px';
2203+
popup.style.overflowY = 'auto';
2204+
}
21772205
}, 200);
21782206
} else {
21792207
// 如果隐藏,则显示
@@ -2182,52 +2210,73 @@ class Live2DManager {
21822210
popup.style.opacity = '0';
21832211
popup.style.visibility = 'visible';
21842212

2185-
// 等待一帧让浏览器计算布局
2186-
setTimeout(() => {
2187-
const popupRect = popup.getBoundingClientRect();
2188-
const screenWidth = window.innerWidth;
2189-
const screenHeight = window.innerHeight;
2190-
const rightMargin = 20; // 距离屏幕右侧的安全边距
2191-
const bottomMargin = 60; // 距离屏幕底部的安全边距(考虑系统任务栏,Windows任务栏约40-48px)
2192-
2193-
// 检查是否超出屏幕右侧
2194-
const popupRight = popupRect.right;
2195-
if (popupRight > screenWidth - rightMargin) {
2196-
// 超出右边界,改为向左弹出
2197-
// 获取按钮的实际宽度来计算正确的偏移
2198-
const button = document.getElementById(`live2d-btn-${buttonId}`);
2199-
const buttonWidth = button ? button.offsetWidth : 48;
2200-
const gap = 8;
2201-
2202-
// 让弹出框完全移到按钮左侧,不遮挡按钮
2203-
popup.style.left = 'auto';
2204-
popup.style.right = '0';
2205-
popup.style.marginLeft = '0';
2206-
popup.style.marginRight = `${buttonWidth + gap}px`;
2207-
popup.style.transform = 'translateX(10px)'; // 反向动画
2213+
// 关键:在计算位置之前,先移除高度限制,确保获取真实尺寸
2214+
if (buttonId === 'settings' || buttonId === 'agent') {
2215+
popup.style.maxHeight = 'none';
2216+
popup.style.overflowY = 'visible';
2217+
}
2218+
2219+
// 等待popup内的所有图片加载完成,确保尺寸准确
2220+
const images = popup.querySelectorAll('img');
2221+
const imageLoadPromises = Array.from(images).map(img => {
2222+
if (img.complete) {
2223+
return Promise.resolve();
22082224
}
2225+
return new Promise(resolve => {
2226+
img.onload = resolve;
2227+
img.onerror = resolve; // 即使加载失败也继续
2228+
// 超时保护:最多等待100ms
2229+
setTimeout(resolve, 100);
2230+
});
2231+
});
2232+
2233+
Promise.all(imageLoadPromises).then(() => {
2234+
// 强制触发reflow,确保布局完全更新
2235+
void popup.offsetHeight;
22092236

2210-
// 检查是否超出屏幕底部(设置弹出框或其他较高的弹出框)
2211-
if (buttonId === 'settings' || buttonId === 'agent') {
2212-
const popupBottom = popupRect.bottom;
2213-
if (popupBottom > screenHeight - bottomMargin) {
2214-
// 计算需要向上移动的距离
2215-
const overflow = popupBottom - (screenHeight - bottomMargin);
2216-
const currentTop = parseInt(popup.style.top) || 0;
2217-
const newTop = currentTop - overflow;
2218-
popup.style.top = `${newTop}px`;
2237+
// 再次使用RAF确保布局稳定
2238+
requestAnimationFrame(() => {
2239+
const popupRect = popup.getBoundingClientRect();
2240+
const screenWidth = window.innerWidth;
2241+
const screenHeight = window.innerHeight;
2242+
const rightMargin = 20; // 距离屏幕右侧的安全边距
2243+
const bottomMargin = 60; // 距离屏幕底部的安全边距(考虑系统任务栏,Windows任务栏约40-48px)
2244+
2245+
// 检查是否超出屏幕右侧
2246+
const popupRight = popupRect.right;
2247+
if (popupRight > screenWidth - rightMargin) {
2248+
// 超出右边界,改为向左弹出
2249+
// 获取按钮的实际宽度来计算正确的偏移
2250+
const button = document.getElementById(`live2d-btn-${buttonId}`);
2251+
const buttonWidth = button ? button.offsetWidth : 48;
2252+
const gap = 8;
2253+
2254+
// 让弹出框完全移到按钮左侧,不遮挡按钮
2255+
popup.style.left = 'auto';
2256+
popup.style.right = '0';
2257+
popup.style.marginLeft = '0';
2258+
popup.style.marginRight = `${buttonWidth + gap}px`;
2259+
popup.style.transform = 'translateX(10px)'; // 反向动画
22192260
}
22202261

2221-
// 取消maxHeight限制,让内容完整显示
2222-
popup.style.maxHeight = 'none';
2223-
popup.style.overflowY = 'visible';
2224-
}
2225-
2226-
// 显示弹出框
2227-
popup.style.visibility = 'visible';
2228-
popup.style.opacity = '1';
2229-
popup.style.transform = 'translateX(0)';
2230-
}, 10);
2262+
// 检查是否超出屏幕底部(设置弹出框或其他较高的弹出框)
2263+
if (buttonId === 'settings' || buttonId === 'agent') {
2264+
const popupBottom = popupRect.bottom;
2265+
if (popupBottom > screenHeight - bottomMargin) {
2266+
// 计算需要向上移动的距离
2267+
const overflow = popupBottom - (screenHeight - bottomMargin);
2268+
const currentTop = parseInt(popup.style.top) || 0;
2269+
const newTop = currentTop - overflow;
2270+
popup.style.top = `${newTop}px`;
2271+
}
2272+
}
2273+
2274+
// 显示弹出框
2275+
popup.style.visibility = 'visible';
2276+
popup.style.opacity = '1';
2277+
popup.style.transform = 'translateX(0)';
2278+
});
2279+
});
22312280

22322281
// 设置、agent、麦克风弹出框不自动隐藏,其他的1秒后隐藏
22332282
if (buttonId !== 'settings' && buttonId !== 'agent' && buttonId !== 'mic') {

templates/chara_manager.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,22 @@ <h2 style="margin: 0;">角色管理</h2>
373373
await showAlert('只剩一只猫娘,无法删除!');
374374
return;
375375
}
376+
// 检查是否是当前猫娘
377+
try {
378+
const currentResponse = await fetch('/api/characters/current_catgirl');
379+
const currentData = await currentResponse.json();
380+
const currentCatgirl = currentData.current_catgirl || '';
381+
382+
if (key === currentCatgirl) {
383+
await showAlert('不能删除当前正在使用的猫娘!\n\n请先切换到其他猫娘后再删除。');
384+
return;
385+
}
386+
} catch (error) {
387+
console.error('获取当前猫娘失败:', error);
388+
await showAlert('无法确认当前猫娘状态,删除操作已取消');
389+
return;
390+
}
391+
376392
if (!await showConfirm('确定要删除猫娘"' + key + '"?', '删除猫娘', {danger: true})) return;
377393
await fetch('/api/characters/catgirl/' + encodeURIComponent(key), {method: 'DELETE'});
378394
await loadCharacterData();

0 commit comments

Comments
 (0)