TH1/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideModView.cs
2026-05-10 11:52:37 +08:00

1573 lines
56 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using Logic.Multilingual;
using Steamworks;
using TH1_Core.Events;
using TH1_Logic.Config;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using ConfigManager = TH1_Logic.Config.ConfigManager;
namespace TH1_UI.View.Outside
{
/// <summary>
/// Mod 管理界面mainOption 切换 SetLanguagePanel / ModPanel。
/// 本期主要实现 ModPanel左侧列表 + 右侧详情,支持按 MultilingualType 筛选。
/// </summary>
public class UIOutsideModView : Base.View
{
// 单次查询/列表上限超出截断Steam 一次最多返回 50 条/页,本 UI 当前只展示首页)
public const int MaxListItemCount = 100;
[Header("通用按钮")]
public Button CloseButton;
[Header("主分页")]
public UIOutsideSelectOptionGroupMono MainOption; // 0=SetLanguagePanel, 1=ModPanel, 2=MakeModePanel
public GameObject SetLanguagePanel;
public GameObject ModPanel;
public GameObject MakeModePanel;
[Header("MakeModePanel - 导出")]
// 导出参数
public TMP_Dropdown ExportTargetLanguageDropdown; // 目标语言mod_info.targetLanguage
public TMP_Dropdown ExportReferenceLanguageDropdown; // 参考语言CSV 第3列
public TMP_InputField ExportModNameInput; // mod 名(留空自动生成)
// 两个导出按钮
public Button ExportCoreButton; // 导出核心文案(暂未实现,待补 IsCore 字段)
public Button ExportFullButton; // 导出全量文案
// 每个按钮各自的状态/反馈文本("正在导出中..." / "导出成功!路径:..."
public TextMeshProUGUI ExportCoreStatusText;
public TextMeshProUGUI ExportFullStatusText;
public Button ExportOpenFolderButton; // 在文件浏览器中打开导出目录(最近一次成功导出)
private string _lastExportPath;
[Header("MakeModePanel - 上传到 Workshop")]
// 选哪个本地 mod 文件夹(自动列出 WorkshopMods/ 下所有目录)
public TMP_Dropdown UploadModFolderDropdown;
// 上传 metadata
public TMP_InputField UploadTitleInput;
public TMP_InputField UploadDescriptionInput;
// 检测到的预览图状态文字("preview.png ✅ 已检测" / "未提供"
public TextMeshProUGUI UploadPreviewStatusText;
// 创建新 / 更新已有 互斥 toggle
public Toggle UploadCreateNewToggle;
public Toggle UploadUpdateExistingToggle;
// 仅在"更新已有"模式可见,列出我已上传的 mod 让玩家选
public TMP_Dropdown UploadUpdateTargetDropdown;
// 操作按钮 + 状态
public Button UploadStartButton;
public TextMeshProUGUI UploadStatusText;
public Button UploadOpenInSteamButton; // 上传成功后亮起,点了跳浏览器
// 协议未签时显示
public GameObject UploadAgreementHint;
public Button UploadAgreementOpenButton;
[Header("MakeModePanel - 我已上传的 Mod 列表")]
public Transform UploadedModListContent;
public GameObject UploadedModListItemPrefab; // 挂 UIOutsideUploadedModListItemMono
public Button UploadedModRefreshButton;
public TextMeshProUGUI UploadedModListStatusText; // "查询中..." / "共 N 项"
[Header("SetLanguagePanel - 当前语言")]
// 当前正在使用的语言下拉。改动 = 立即 ChangedMultilingual等价于设置面板里切语言。
public TMP_Dropdown CurrentLanguageDropdown;
[Header("SetLanguagePanel - 正在使用 (左)")]
// 当前选中语言启用的 mod 列表父节点。
// 显示顺序:从上到下 = 高优先级 → 低优先级,最底部固定一个不可删除的"系统项"
public Transform ActiveModListContent;
[Header("SetLanguagePanel - 配置入口")]
public Button StartConfigButton; // 默认显示,点击后进入 Config 模式
public GameObject ConfigPart; // 默认隐藏,进入 Config 模式后整体显示
[Header("SetLanguagePanel - ConfigPart")]
public Transform InstalledModListContent; // 已安装 mod 列表父节点
public Toggle ShowAllToggle; // 勾选=全部已安装;不勾=仅匹配当前语言(默认不勾)
public Button SaveConfigButton; // 保存并应用
public Button CancelConfigButton; // 取消(丢弃副本)
[Header("ModPanel - 左侧列表")]
public Transform ListContent; // 列表行的 parent
public GameObject ListItemPrefab; // 挂 UIOutsideModListItemMono
public Button StartQueryButton; // 触发 Steam 工坊查询
public TextMeshProUGUI QueryStatusText; // "查询中..." / "未查询" / "已完成 N 条"
public TMP_Dropdown LanguageFilterDropdown; // None=不限 + 41 种语言(自己填或代码 init
[Header("ModPanel - 右侧详情")]
public GameObject DetailRoot; // 整个详情容器(无选中时隐藏)
public TextMeshProUGUI TitleText;
public TextMeshProUGUI MainLanguageTagText;
public TextMeshProUGUI AuthorText;
public TextMeshProUGUI TimeText;
public TextMeshProUGUI DescText;
[Header("ModPanel - 详情订阅状态")]
public GameObject SubscribedHint; // "已订阅" 提示
public GameObject InstalledHint; // "已安装" 提示
public GameObject UnsubscribedHint; // "未订阅" 提示
public Button SubscribeButton;
public Button UnsubscribeButton;
// 关闭时执行的委托
public ViDelegateAssisstant.Dele OnBtnCloseClick;
// 内部数据
private readonly List<UIOutsideModListData> _allMods = new List<UIOutsideModListData>();
private readonly List<UIOutsideModListItemMono> _itemPool = new List<UIOutsideModListItemMono>();
private MultilingualType _languageFilter = MultilingualType.None;
private UIOutsideModListData _selected;
private bool _hasQueriedWorkshop;
// 当前 ModPanel 列表过滤后实际渲染的条数。RefreshList 写入UpdateQueryStatus 读取
// 给"已加载 N/Total"做分子,确保切语言筛选/订阅后状态文字立刻同步当前所见。
private int _visibleModCount;
// SetLanguagePanel 内部数据
// 当前编辑/展示的目标语言(跟 MultilingualManager.CurrentType 同步dropdown 改它)
private MultilingualType _editingLanguage = MultilingualType.EN;
// 是否处于 Config 模式(点了 StartConfig正在编辑 mod 列表)
private bool _isInConfigMode;
// Config 模式下编辑用的副本(保存才写回 ConfigManager.Config
// 仅缓存当前编辑语言这一份;切语言或退出会丢
private List<string> _editingModPaths = new List<string>();
// "已安装 mod" 数据:本地 + 已订阅安装。供 Active 列表查 LocalFolder 复用
private readonly List<UIOutsideModListData> _availableMods = new List<UIOutsideModListData>();
private readonly List<UIOutsideModListItemMono> _activeItemPool = new List<UIOutsideModListItemMono>();
private readonly List<UIOutsideModListItemMono> _availableItemPool = new List<UIOutsideModListItemMono>();
// CurrentLanguageDropdown 的索引到 MultilingualType 的映射
private readonly List<MultilingualType> _languageOptions = new List<MultilingualType>();
// MakeModPanel 两个 Dropdown 的索引到 MultilingualType 的映射(与 _languageOptions 内容相同,但分开持有避免耦合)
private readonly List<MultilingualType> _exportLanguageOptions = new List<MultilingualType>();
// 上传子模块的内部数据
private readonly List<string> _uploadFolderOptions = new List<string>(); // dropdown 索引 → 文件夹绝对路径
private readonly List<PublishedFileId_t> _uploadUpdateTargets = new List<PublishedFileId_t>(); // dropdown 索引 → fileId
private PublishedFileId_t _lastUploadedFileId;
private readonly List<UIOutsideUploadedModListItemMono> _uploadedItemPool = new List<UIOutsideUploadedModListItemMono>();
// 5 种主语言常量(其余语言走系统项 = Default English
private static readonly MultilingualType[] PrimaryLanguages =
{
MultilingualType.ZH, MultilingualType.TDZH, MultilingualType.EN,
MultilingualType.JP, MultilingualType.KR,
};
protected override void OnInit()
{
base.OnInit();
if (CloseButton != null)
{
CloseButton.onClick.RemoveAllListeners();
CloseButton.onClick.AddListener(OnClose);
}
if (StartQueryButton != null)
{
StartQueryButton.onClick.RemoveAllListeners();
StartQueryButton.onClick.AddListener(OnStartQueryClicked);
}
if (SubscribeButton != null)
{
SubscribeButton.onClick.RemoveAllListeners();
SubscribeButton.onClick.AddListener(OnSubscribeClicked);
}
if (UnsubscribeButton != null)
{
UnsubscribeButton.onClick.RemoveAllListeners();
UnsubscribeButton.onClick.AddListener(OnUnsubscribeClicked);
}
if (StartConfigButton != null)
{
StartConfigButton.onClick.RemoveAllListeners();
StartConfigButton.onClick.AddListener(OnStartConfigClicked);
}
if (SaveConfigButton != null)
{
SaveConfigButton.onClick.RemoveAllListeners();
SaveConfigButton.onClick.AddListener(OnSaveConfigClicked);
}
if (CancelConfigButton != null)
{
CancelConfigButton.onClick.RemoveAllListeners();
CancelConfigButton.onClick.AddListener(OnCancelConfigClicked);
}
if (ShowAllToggle != null)
{
ShowAllToggle.onValueChanged.RemoveAllListeners();
ShowAllToggle.onValueChanged.AddListener(_ => RenderAvailableList());
}
if (ExportCoreButton != null)
{
ExportCoreButton.onClick.RemoveAllListeners();
ExportCoreButton.onClick.AddListener(OnExportCoreClicked);
}
if (ExportFullButton != null)
{
ExportFullButton.onClick.RemoveAllListeners();
ExportFullButton.onClick.AddListener(OnExportFullClicked);
}
if (ExportOpenFolderButton != null)
{
ExportOpenFolderButton.onClick.RemoveAllListeners();
ExportOpenFolderButton.onClick.AddListener(OnExportOpenFolderClicked);
}
// 上传子模块按钮
if (UploadStartButton != null)
{
UploadStartButton.onClick.RemoveAllListeners();
UploadStartButton.onClick.AddListener(OnUploadStartClicked);
}
if (UploadOpenInSteamButton != null)
{
UploadOpenInSteamButton.onClick.RemoveAllListeners();
UploadOpenInSteamButton.onClick.AddListener(OnUploadOpenInSteamClicked);
}
if (UploadAgreementOpenButton != null)
{
UploadAgreementOpenButton.onClick.RemoveAllListeners();
UploadAgreementOpenButton.onClick.AddListener(OnUploadAgreementOpenClicked);
}
if (UploadCreateNewToggle != null)
{
UploadCreateNewToggle.onValueChanged.RemoveAllListeners();
UploadCreateNewToggle.onValueChanged.AddListener(OnUploadModeToggleChanged);
}
if (UploadUpdateExistingToggle != null)
{
UploadUpdateExistingToggle.onValueChanged.RemoveAllListeners();
UploadUpdateExistingToggle.onValueChanged.AddListener(OnUploadModeToggleChanged);
}
if (UploadedModRefreshButton != null)
{
UploadedModRefreshButton.onClick.RemoveAllListeners();
UploadedModRefreshButton.onClick.AddListener(OnUploadedModRefreshClicked);
}
// Steam 异步回调统一通过 Browser 注册到 OnQueryCompleted/OnSubscribeCompleted
WorkshopModBrowser.Instance.OnQueryCompleted += OnWorkshopQueryCompleted;
WorkshopModBrowser.Instance.OnSubscribeCompleted += OnSteamSubscribeCompleted;
WorkshopModBrowser.Instance.OnUnsubscribeCompleted += OnSteamUnsubscribeCompleted;
WorkshopModBrowser.Instance.OnUserModsQueryCompleted += OnUserModsQueryCompleted;
}
private void OnDestroy()
{
WorkshopModBrowser.Instance.OnQueryCompleted -= OnWorkshopQueryCompleted;
WorkshopModBrowser.Instance.OnSubscribeCompleted -= OnSteamSubscribeCompleted;
WorkshopModBrowser.Instance.OnUnsubscribeCompleted -= OnSteamUnsubscribeCompleted;
WorkshopModBrowser.Instance.OnUserModsQueryCompleted -= OnUserModsQueryCompleted;
}
// Steam 回调需要主循环调用 RunCallbacks本 View 处于活动期间持续轮询
private void Update()
{
WorkshopModBrowser.Instance.RunCallbacks();
// Uploader 是基于轮询 IsAPICallCompleted 的,每帧 Poll 推动状态机
if (WorkshopModUploader.Instance.IsBusy)
{
WorkshopModUploader.Instance.Poll();
RefreshUploadButtonInteractable();
if (UploadStatusText != null) UploadStatusText.text = WorkshopModUploader.Instance.StatusMessage;
}
}
public void SetContent(ShowUIOutsideMod evt)
{
// 必须先禁用 Dropdown caption/item Label 上的 MultilingualTextMono
// prefab 残留 ID 会在切语言时覆盖代码 AddOptions 写入的文字。Init 内 AddOptions
// 会立刻触发 captionText 重绘,所以 Ban 要先于 Init 执行。
DisableDropdownLabelMultilingual(CurrentLanguageDropdown);
DisableDropdownLabelMultilingual(LanguageFilterDropdown);
DisableDropdownLabelMultilingual(ExportTargetLanguageDropdown);
DisableDropdownLabelMultilingual(ExportReferenceLanguageDropdown);
DisableDropdownLabelMultilingual(UploadModFolderDropdown);
DisableDropdownLabelMultilingual(UploadUpdateTargetDropdown);
InitMainOption();
InitLanguageFilterDropdown();
InitCurrentLanguageDropdown();
InitExportLanguageDropdowns();
_hasQueriedWorkshop = false;
ClearSelection();
RefreshList(); // 内部已联动 UpdateQueryStatus
// 进入面板默认非 Config 模式,编辑语言 = 当前正在用的语言
ExitConfigMode(discardChanges: true);
RefreshLanguagePanel();
ResetExportStatus();
InitUploadModule();
QueryUserModsAndRefresh();
}
// 在 Controller 调用 Close 时会先调用这里
public void OnCloseView()
{
}
public void OnClose()
{
OnBtnCloseClick?.Invoke();
}
// ── mainOption 初始化 ──
private void InitMainOption()
{
if (MainOption == null) return;
// 先把目标面板可见性切到默认 (Option 0 = SetLanguagePanel)
// 否则 SelectOptionGroup.Init -> Select -> LayoutRebuilder
// 在 inactive 状态下计算 anchoredPosition 会拿到错误值,导致 SelectBG 跑偏。
ApplyMainOptionVisibility(0);
MainOption.Init(0); // 默认选 Option 0 = SetLanguagePanel
MainOption.OnOptionClicked = OnMainOptionClicked;
}
private void OnMainOptionClicked(uint idx)
{
ApplyMainOptionVisibility(idx);
// 切回语言面板时强制退出 Config 模式 + 重新拉一遍(覆盖期间订阅/安装变化)
if (idx == 0)
{
ExitConfigMode(discardChanges: true);
RefreshLanguagePanel();
}
// 切到 Mod 面板时也重拉一次:语言面板里订阅/取消订阅会改 mod 数据,
// 否则 ModPanel 列表 + QueryStatusText 都会停留在旧值
else if (idx == 1)
{
RefreshList();
}
}
private void ApplyMainOptionVisibility(uint idx)
{
if (SetLanguagePanel != null) SetLanguagePanel.SetActive(idx == 0);
if (ModPanel != null) ModPanel.SetActive(idx == 1);
if (MakeModePanel != null) MakeModePanel.SetActive(idx == 2);
}
// ── 语言筛选下拉 ──
// 注Inspector 里也可以预先填好 options这里用代码兜底
private void InitLanguageFilterDropdown()
{
if (LanguageFilterDropdown == null) return;
LanguageFilterDropdown.ClearOptions();
var options = new List<string> { "All" };
var values = new List<MultilingualType> { MultilingualType.None };
foreach (MultilingualType t in Enum.GetValues(typeof(MultilingualType)))
{
if (t == MultilingualType.None || t == MultilingualType.Max) continue;
options.Add(GetLanguageDisplayName(t));
values.Add(t);
}
LanguageFilterDropdown.AddOptions(options);
LanguageFilterDropdown.value = 0;
_languageFilter = MultilingualType.None;
// 兜底刷一次 caption避免 captionText 残留 prefab 上的占位文本)
if (LanguageFilterDropdown.captionText != null)
{
LanguageFilterDropdown.captionText.text = options[0];
}
LanguageFilterDropdown.onValueChanged.RemoveAllListeners();
LanguageFilterDropdown.onValueChanged.AddListener(i =>
{
_languageFilter = i >= 0 && i < values.Count ? values[i] : MultilingualType.None;
RefreshList();
});
}
// ── 列表刷新 ──
// 重新拉取本地+订阅 Mod与已查询的 Workshop 结果合并,按筛选条件渲染
private void RefreshList()
{
RebuildAllModsCache();
var filtered = FilterMods(_allMods, _languageFilter);
if (filtered.Count > MaxListItemCount)
filtered = filtered.GetRange(0, MaxListItemCount);
RenderList(filtered);
_visibleModCount = filtered.Count;
// 列表为空 → 清空详情;
// 当前选中项已不在过滤结果里 → 默认选中首项;
// 当前选中项仍在 → 保持选中。
if (filtered.Count == 0)
{
ClearSelection();
}
else if (_selected == null || !filtered.Contains(_selected))
{
OnListItemSelected(filtered[0]);
}
else
{
// 已选项还在,但 _itemPool 顺序可能变了,重新刷一遍 highlight
RefreshHighlight();
}
// 列表唯一变更入口在这里,状态文字一并联动,避免分散在多处遗漏
UpdateQueryStatus();
}
private void RebuildAllModsCache()
{
_allMods.Clear();
// 1. 本地 Mod包括已安装的订阅 Mod因为 GetSubscribedModPaths 也读 mod_info.json
AppendLocalMods(_allMods, WorkshopModLoader.GetLocalModPaths(), ModListItemSource.Local);
AppendLocalMods(_allMods, WorkshopModLoader.GetSubscribedModPaths(), ModListItemSource.Subscribed);
// 2. Steam 工坊查询结果(仅在用户点过查询按钮后才有)
if (_hasQueriedWorkshop)
{
foreach (var item in WorkshopModBrowser.Instance.Results)
{
_allMods.Add(new UIOutsideModListData
{
Source = ModListItemSource.Workshop,
Title = item.Title,
Author = "", // Steam 查询拿的是 OwnerSteamId作者名要后续 RequestUserInformation 才能拿
Description = item.Description,
TargetLanguage = MultilingualType.None, // 在线 Mod 在订阅安装前拿不到 mod_infoTargetLanguage 留空
SteamFileId = item.FileId,
SteamIsSubscribed = item.IsSubscribed,
SteamIsInstalled = item.IsInstalled,
SteamVotesUp = item.VotesUp,
SteamVotesDown = item.VotesDown,
SteamFileSize = item.FileSize,
SteamCreatedTime = item.CreatedTime,
SteamUpdatedTime = item.UpdatedTime,
LocalFolder = item.InstallFolder,
});
}
}
}
private static void AppendLocalMods(List<UIOutsideModListData> dst, List<string> folders, ModListItemSource source)
{
foreach (var folder in folders)
{
var info = WorkshopModExporter.ReadModInfo(folder);
if (info == null) continue;
MultilingualType target = MultilingualType.None;
if (Enum.TryParse<MultilingualType>(info.targetLanguage, true, out var parsed))
target = parsed;
dst.Add(new UIOutsideModListData
{
Source = source,
Title = string.IsNullOrEmpty(info.title) ? System.IO.Path.GetFileName(folder) : info.title,
Author = info.author ?? "",
Description = info.description ?? "",
TargetLanguage = target,
Version = info.version ?? "",
LocalFolder = folder,
});
}
}
private static List<UIOutsideModListData> FilterMods(List<UIOutsideModListData> src, MultilingualType filter)
{
if (filter == MultilingualType.None) return new List<UIOutsideModListData>(src);
var result = new List<UIOutsideModListData>();
foreach (var m in src)
{
if (m.TargetLanguage == filter) result.Add(m);
}
return result;
}
// 复用对象池:不够补足,多余隐藏
private void RenderList(List<UIOutsideModListData> mods)
{
while (_itemPool.Count < mods.Count)
{
if (ListItemPrefab == null || ListContent == null) break;
var go = Instantiate(ListItemPrefab, ListContent);
var mono = go.GetComponent<UIOutsideModListItemMono>();
if (mono == null) { Destroy(go); break; }
_itemPool.Add(mono);
}
for (int i = 0; i < _itemPool.Count; i++)
{
bool active = i < mods.Count;
_itemPool[i].gameObject.SetActive(active);
if (!active) continue;
_itemPool[i].SetContent(mods[i]);
_itemPool[i].OnSelected = OnListItemSelected;
_itemPool[i].OnAddClicked = null;
_itemPool[i].OnRemoveClicked = null;
_itemPool[i].SetButtonVisibility(showAdd: false, showRemove: false);
_itemPool[i].SetHighlight(_selected != null && ReferenceEquals(_selected, mods[i]));
}
}
private void OnListItemSelected(UIOutsideModListData data)
{
_selected = data;
RefreshDetail();
RefreshHighlight();
}
private void RefreshHighlight()
{
foreach (var item in _itemPool)
{
if (!item.gameObject.activeSelf) continue;
item.SetHighlight(_selected != null && ReferenceEquals(item.GetData(), _selected));
}
}
private void ClearSelection()
{
_selected = null;
RefreshDetail();
}
// ── 详情面板 ──
private void RefreshDetail()
{
if (DetailRoot != null) DetailRoot.SetActive(_selected != null);
if (_selected == null) return;
if (TitleText != null) TitleText.text = _selected.Title;
if (MainLanguageTagText != null) MainLanguageTagText.text = _selected.TargetLanguage.ToString();
if (AuthorText != null) AuthorText.text = _selected.Author;
if (DescText != null) DescText.text = _selected.Description;
if (TimeText != null)
{
if (_selected.SteamUpdatedTime > 0)
{
var dt = DateTimeOffset.FromUnixTimeSeconds(_selected.SteamUpdatedTime).LocalDateTime;
TimeText.text = dt.ToString("yyyy-MM-dd HH:mm");
}
else
{
TimeText.text = string.IsNullOrEmpty(_selected.Version) ? "" : $"v{_selected.Version}";
}
}
RefreshSubscribeUI();
}
// 订阅状态 UI3 个 hint 互斥 + 2 个按钮按状态显示
private void RefreshSubscribeUI()
{
bool hasSteamId = _selected != null && _selected.SteamFileId.m_PublishedFileId != 0;
bool isSubscribed = _selected?.SteamIsSubscribed ?? false;
bool isInstalled = _selected?.SteamIsInstalled ?? false;
// 本地 Mod非来自 Workshop 订阅)也算"已安装"展示
if (_selected != null && _selected.Source == ModListItemSource.Local) isInstalled = true;
if (_selected != null && _selected.Source == ModListItemSource.Subscribed) { isSubscribed = true; isInstalled = true; }
if (SubscribedHint != null) SubscribedHint.SetActive(isSubscribed && !isInstalled);
if (InstalledHint != null) InstalledHint.SetActive(isInstalled);
if (UnsubscribedHint != null) UnsubscribedHint.SetActive(hasSteamId && !isSubscribed && !isInstalled);
// 仅 Workshop 来源的 mod 才有"订阅/取消订阅"操作(本地 mod 不能取消订阅)
if (SubscribeButton != null) SubscribeButton.gameObject.SetActive(hasSteamId && !isSubscribed);
if (UnsubscribeButton != null) UnsubscribeButton.gameObject.SetActive(hasSteamId && isSubscribed);
}
// ── 查询按钮 ──
private void OnStartQueryClicked()
{
if (WorkshopModBrowser.Instance.IsQuerying) return;
WorkshopModBrowser.Instance.QueryPage(1);
UpdateQueryStatus();
}
private void OnWorkshopQueryCompleted()
{
_hasQueriedWorkshop = true;
RefreshList(); // 内部已联动 UpdateQueryStatus
}
private void UpdateQueryStatus()
{
if (QueryStatusText == null) return;
var browser = WorkshopModBrowser.Instance;
var assets = Table.Instance.TextDataAssets;
if (browser.IsQuerying)
{
MultilingualManager.Instance.SetUIText(QueryStatusText, assets.OutsideModQueryInProgress);
}
else if (!_hasQueriedWorkshop)
{
MultilingualManager.Instance.SetUIText(QueryStatusText, assets.OutsideModQueryNotStarted);
}
else if (!string.IsNullOrEmpty(browser.LastError))
{
// 错误信息直接覆盖(已是最易诊断的英文 Steam 返回,不走多语言)
QueryStatusText.text = browser.LastError;
}
else
{
// 分子 = 列表当前过滤后实际可见数;分母 = Workshop 总条数
// 这样玩家切语言/筛选条件后状态文字直接反映"我现在看到了多少"
MultilingualManager.Instance.SetUIText(
QueryStatusText,
assets.OutsideModQueryLoaded,
new List<string> { _visibleModCount.ToString(), browser.TotalResults.ToString() });
}
}
// ── 订阅/取消订阅 ──
private void OnSubscribeClicked()
{
if (_selected == null || _selected.SteamFileId.m_PublishedFileId == 0) return;
WorkshopModBrowser.Instance.Subscribe(_selected.SteamFileId);
}
private void OnUnsubscribeClicked()
{
if (_selected == null || _selected.SteamFileId.m_PublishedFileId == 0) return;
WorkshopModBrowser.Instance.Unsubscribe(_selected.SteamFileId);
}
private void OnSteamSubscribeCompleted(PublishedFileId_t fileId, bool success)
{
if (!success) return;
SyncSelectedFromBrowser(fileId);
}
private void OnSteamUnsubscribeCompleted(PublishedFileId_t fileId, bool success)
{
if (!success) return;
SyncSelectedFromBrowser(fileId);
}
private void SyncSelectedFromBrowser(PublishedFileId_t fileId)
{
var item = WorkshopModBrowser.Instance.Results.Find(r => r.FileId == fileId);
if (item != null && _selected != null && _selected.SteamFileId == fileId)
{
_selected.SteamIsSubscribed = item.IsSubscribed;
_selected.SteamIsInstalled = item.IsInstalled;
_selected.LocalFolder = item.InstallFolder;
RefreshSubscribeUI();
}
// 订阅状态变化会改变本地+订阅 mod 集合(已订阅 mod 会出现在 GetSubscribedModPaths 里),
// 重拉一次列表让 ModPanel 与 QueryStatusText 都同步到最新状态
RefreshList();
}
// ── SetLanguagePanel当前语言下拉 ──
// 当前语言下拉,改动 = 立即 ChangedMultilingual。如处于 Config 模式则隐式取消(丢弃副本)
private void InitCurrentLanguageDropdown()
{
if (CurrentLanguageDropdown == null) return;
CurrentLanguageDropdown.ClearOptions();
_languageOptions.Clear();
var options = new List<string>();
foreach (MultilingualType t in Enum.GetValues(typeof(MultilingualType)))
{
if (t == MultilingualType.None || t == MultilingualType.Max) continue;
_languageOptions.Add(t);
options.Add(GetLanguageDisplayName(t));
}
CurrentLanguageDropdown.AddOptions(options);
SyncDropdownToCurrent();
CurrentLanguageDropdown.onValueChanged.RemoveAllListeners();
CurrentLanguageDropdown.onValueChanged.AddListener(OnCurrentLanguageChanged);
}
private void SyncDropdownToCurrent()
{
if (CurrentLanguageDropdown == null) return;
var cur = MultilingualManager.Instance.CurrentType;
int idx = _languageOptions.IndexOf(cur);
if (idx < 0) idx = _languageOptions.IndexOf(MultilingualType.EN);
if (idx < 0) idx = 0;
CurrentLanguageDropdown.SetValueWithoutNotify(idx);
// SetValueWithoutNotify 在新值 == 旧值时不会刷 caption这里兜底直接写一次
if (CurrentLanguageDropdown.captionText != null && idx < _languageOptions.Count)
{
CurrentLanguageDropdown.captionText.text = GetLanguageDisplayName(_languageOptions[idx]);
}
}
private void OnCurrentLanguageChanged(int i)
{
if (i < 0 || i >= _languageOptions.Count) return;
// 切语言 = 隐式取消 Config 模式(按 PRD 要求)
if (_isInConfigMode) ExitConfigMode(discardChanges: true);
var lang = _languageOptions[i];
MultilingualManager.Instance.ChangedMultilingual(lang);
// 切到非主语言(中英日韩繁以外)时,把 SecondaryLanguage 同步成它,
// 这样 Setting 面板会停在 MoreLanguage 选项并展开 MoreLanguageModule状态保持一致
if (!IsPrimaryLanguageStatic(lang))
{
ConfigManager.Instance.Config.SecondaryLanguage = lang;
}
RefreshLanguagePanel();
}
private static bool IsPrimaryLanguageStatic(MultilingualType type)
{
return Array.IndexOf(PrimaryLanguages, type) >= 0;
}
// ── SetLanguagePanel刷新 ──
// 入口:根据当前编辑语言(= 当前正在用的语言)刷新 Active 列表 + Available 列表
private void RefreshLanguagePanel()
{
_editingLanguage = MultilingualManager.Instance.CurrentType;
if (_editingLanguage == MultilingualType.None) _editingLanguage = MultilingualType.EN;
SyncDropdownToCurrent();
BuildAvailableMods();
LoadEditingModPathsFromConfig();
RenderActiveList();
RenderAvailableList();
}
// 已安装 mod 缓存 = 本地 + 已订阅安装
private void BuildAvailableMods()
{
_availableMods.Clear();
AppendLocalMods(_availableMods, WorkshopModLoader.GetLocalModPaths(), ModListItemSource.Local);
AppendLocalMods(_availableMods, WorkshopModLoader.GetSubscribedModPaths(), ModListItemSource.Subscribed);
}
// 把 Config 里当前编辑语言的 ModPaths 拷贝到 _editingModPaths编辑副本
private void LoadEditingModPathsFromConfig()
{
_editingModPaths.Clear();
var config = ConfigManager.Instance.Config.ModLanguageConfigs.Find(c => c.Language == _editingLanguage);
if (config != null && config.ModPaths != null)
{
_editingModPaths.AddRange(config.ModPaths);
}
}
private static UIOutsideModListData CreateModData(string folder, ModListItemSource source)
{
var info = WorkshopModExporter.ReadModInfo(folder);
if (info == null) return null;
MultilingualType target = MultilingualType.None;
if (Enum.TryParse<MultilingualType>(info.targetLanguage, true, out var parsed))
target = parsed;
return new UIOutsideModListData
{
Source = source,
Title = string.IsNullOrEmpty(info.title) ? System.IO.Path.GetFileName(folder) : info.title,
Author = info.author ?? "",
Description = info.description ?? "",
TargetLanguage = target,
Version = info.version ?? "",
LocalFolder = folder,
};
}
// ── SetLanguagePanel左列表正在使用 ──
// ActiveList = [_editingModPaths 反向构建] + [系统项作为最底部]
// 顶部=高优先级=ModPaths 末尾,底部=系统项(最低优先级,永远存在)
private void RenderActiveList()
{
// 收集需要展示的真实 mod 数据Config 里有但磁盘上 mod 已被删除的,跳过)
var realMods = new List<UIOutsideModListData>();
for (int i = _editingModPaths.Count - 1; i >= 0; i--)
{
var folder = _editingModPaths[i];
var existing = _availableMods.Find(m => m.LocalFolder == folder);
if (existing != null)
{
realMods.Add(existing);
continue;
}
var data = CreateModData(folder, ModListItemSource.Local);
if (data != null) realMods.Add(data);
}
// 总行数 = 真实 mod + 1 个系统项
int total = realMods.Count + 1;
EnsurePoolSize(_activeItemPool, total, ActiveModListContent);
for (int i = 0; i < _activeItemPool.Count; i++)
{
bool active = i < total;
_activeItemPool[i].gameObject.SetActive(active);
if (!active) continue;
var item = _activeItemPool[i];
bool isSystem = i == total - 1;
if (isSystem)
{
var (sysLang, sysTitle) = GetSystemFallbackFor(_editingLanguage);
item.SetAsSystemItem(sysTitle, sysLang);
}
else
{
item.SetContent(realMods[i]);
item.OnSelected = null;
item.OnAddClicked = null;
item.OnRemoveClicked = OnRemoveModClicked;
// 删除按钮仅在 Config 模式下可见
item.SetButtonVisibility(showAdd: false, showRemove: _isInConfigMode);
item.SetHighlight(false);
}
// 系统项确保按钮全隐藏SetAsSystemItem 已经做了,这里冗余保护)
}
// LayoutGroup 排序ScrollView 默认按 sibling index 渲染pool 顺序就是显示顺序
// 让系统项始终在最后(即 pool[total-1] 在所有 active 项里 sibling index 最大)
// 由于 pool 是顺序填的,且系统项放在 total-1已经满足
}
// 当前编辑语言的"系统项" = 5 主语言用自己默认;其他用 Default English
private static (MultilingualType lang, string title) GetSystemFallbackFor(MultilingualType editing)
{
if (Array.IndexOf(PrimaryLanguages, editing) >= 0)
{
return (editing, $"Default {GetEnglishLanguageName(editing)}");
}
return (MultilingualType.EN, "Default English");
}
private static string GetEnglishLanguageName(MultilingualType t)
{
return t switch
{
MultilingualType.ZH => "Chinese",
MultilingualType.TDZH => "Traditional Chinese",
MultilingualType.EN => "English",
MultilingualType.JP => "Japanese",
MultilingualType.KR => "Korean",
_ => t.ToString(),
};
}
// ── SetLanguagePanel右列表已安装 ──
// 仅在 Config 模式下可见。CheckBox 不勾选时按 _editingLanguage 过滤;勾选时显示全部
private void RenderAvailableList()
{
if (!_isInConfigMode || InstalledModListContent == null)
{
// 非 Config 模式或字段未配置:隐藏整个右列表的所有 item
for (int i = 0; i < _availableItemPool.Count; i++)
{
_availableItemPool[i].gameObject.SetActive(false);
}
return;
}
bool showAll = ShowAllToggle != null && ShowAllToggle.isOn;
// 过滤
var filtered = new List<UIOutsideModListData>();
foreach (var m in _availableMods)
{
if (showAll || m.TargetLanguage == _editingLanguage) filtered.Add(m);
}
EnsurePoolSize(_availableItemPool, filtered.Count, InstalledModListContent);
// 已经在 _editingModPaths 里的 mod把右侧 Add 按钮置灰
var activePaths = new HashSet<string>(_editingModPaths);
for (int i = 0; i < _availableItemPool.Count; i++)
{
bool active = i < filtered.Count;
_availableItemPool[i].gameObject.SetActive(active);
if (!active) continue;
var item = _availableItemPool[i];
item.SetContent(filtered[i]);
item.OnSelected = null;
item.OnAddClicked = OnAddModClicked;
item.OnRemoveClicked = null;
item.SetButtonVisibility(showAdd: true, showRemove: false);
item.SetAddInteractable(!activePaths.Contains(filtered[i].LocalFolder));
item.SetHighlight(false);
}
}
private void EnsurePoolSize(List<UIOutsideModListItemMono> pool, int count, Transform parent)
{
if (parent == null || ListItemPrefab == null) return;
while (pool.Count < count)
{
var go = Instantiate(ListItemPrefab, parent);
var mono = go.GetComponent<UIOutsideModListItemMono>();
if (mono == null) { Destroy(go); break; }
pool.Add(mono);
}
}
// ── SetLanguagePanelConfig 模式 ──
private void OnStartConfigClicked()
{
EnterConfigMode();
}
private void OnSaveConfigClicked()
{
// 写回 Config触发还原快照 + 按 Config 重 apply + TMP 重绘
ConfigManager.Instance.Config.SetModsForLanguage(_editingLanguage, _editingModPaths);
MultilingualManager.Instance.SaveAndApplyMods();
ExitConfigMode(discardChanges: false);
RefreshLanguagePanel();
}
private void OnCancelConfigClicked()
{
ExitConfigMode(discardChanges: true);
RefreshLanguagePanel();
}
private void EnterConfigMode()
{
_isInConfigMode = true;
LoadEditingModPathsFromConfig(); // 从 Config 拷贝一份新副本作为编辑起点
ApplyConfigModeUI();
RenderActiveList(); // 让左列表的 Remove 按钮亮起来
RenderAvailableList();
}
private void ExitConfigMode(bool discardChanges)
{
_isInConfigMode = false;
if (discardChanges) _editingModPaths.Clear();
ApplyConfigModeUI();
}
private void ApplyConfigModeUI()
{
if (StartConfigButton != null) StartConfigButton.gameObject.SetActive(!_isInConfigMode);
if (ConfigPart != null) ConfigPart.SetActive(_isInConfigMode);
// 默认 ShowAllToggle 不勾选(仅显示当前语言匹配的 mod
if (_isInConfigMode && ShowAllToggle != null && ShowAllToggle.isOn)
{
ShowAllToggle.SetIsOnWithoutNotify(false);
}
}
// ── SetLanguagePanel增 / 删(仅作用于编辑副本 _editingModPaths ──
// 添加:插到 _editingModPaths 索引 0最低优先级UI 上落在左侧列表最下面(系统项之上)
private void OnAddModClicked(UIOutsideModListData data)
{
if (!_isInConfigMode) return;
if (data == null || string.IsNullOrEmpty(data.LocalFolder)) return;
if (_editingModPaths.Contains(data.LocalFolder)) return;
_editingModPaths.Insert(0, data.LocalFolder);
RenderActiveList();
RenderAvailableList();
}
// 删除:从 _editingModPaths 移除
private void OnRemoveModClicked(UIOutsideModListData data)
{
if (!_isInConfigMode) return;
if (data == null || string.IsNullOrEmpty(data.LocalFolder)) return;
if (!_editingModPaths.Remove(data.LocalFolder)) return;
RenderActiveList();
RenderAvailableList();
}
// 禁用 dropdown 的 captionText / itemText 上挂着的 MultilingualTextMono。
// 历史 prefab 上这俩 Label 自动绑定了占位 ID切语言时 OnMultilingualChanged 会把
// 我们 AddOptions 写入的文字覆盖掉,看到下拉顶上显示的语言名错位。
private static void DisableDropdownLabelMultilingual(TMP_Dropdown dropdown)
{
if (dropdown == null) return;
TrySetMultilingualBan(dropdown.captionText);
TrySetMultilingualBan(dropdown.itemText);
}
private static void TrySetMultilingualBan(TMP_Text text)
{
if (text == null) return;
var mono = text.GetComponent<MultilingualTextMono>();
if (mono == null) return;
mono.Ban = true;
}
// 第二语言显示名:与 UIOutsideMenuSettingPanelMono / UITopSettingView 保持一致
private static string GetLanguageDisplayName(MultilingualType type)
{
return type switch
{
MultilingualType.ZH => "简体中文",
MultilingualType.TDZH => "繁體中文",
MultilingualType.EN => "English",
MultilingualType.JP => "日本語",
MultilingualType.KR => "한국어",
MultilingualType.RU => "Русский",
MultilingualType.ES => "Español",
MultilingualType.PT => "Português",
MultilingualType.FR => "Français",
MultilingualType.DE => "Deutsch",
MultilingualType.ID => "Bahasa Indonesia",
MultilingualType.TH => "ภาษาไทย",
MultilingualType.PL => "Polski",
MultilingualType.VI => "Tiếng Việt",
MultilingualType.MS => "Bahasa Melayu",
MultilingualType.UK => "Українська",
MultilingualType.KZ => "Қазақша",
MultilingualType.TR => "Türkçe",
MultilingualType.IT => "Italiano",
MultilingualType.NL => "Nederlands",
MultilingualType.FI => "Suomi",
MultilingualType.SV => "Svenska",
MultilingualType.NO => "Norsk",
MultilingualType.CS => "Čeština",
MultilingualType.HU => "Magyar",
MultilingualType.EL => "Ελληνικά",
MultilingualType.RO => "Română",
MultilingualType.ET => "Eesti",
MultilingualType.LT => "Lietuvių",
MultilingualType.HR => "Hrvatski",
MultilingualType.SR => "Српски",
MultilingualType.SL => "Slovenščina",
MultilingualType.SK => "Slovenčina",
MultilingualType.BE => "Беларуская",
MultilingualType.HE => "עברית",
MultilingualType.BG => "Български",
MultilingualType.UZ => "Oʻzbekcha",
MultilingualType.KY => "Кыргызча",
MultilingualType.MN => "Монгол",
MultilingualType.AR => "العربية",
MultilingualType.DA => "Dansk",
MultilingualType.TL => "Filipino",
MultilingualType.Custom => "Custom",
_ => type.ToString()
};
}
// ── MakeModePanel导出模板 ──
// 初始化两个语言下拉。默认目标=当前语言(玩家最常见用例:给自己的语言导个模板继续翻译/校对)
// 默认参考=ZH最完整的源语言便于翻译者参考原意
private void InitExportLanguageDropdowns()
{
_exportLanguageOptions.Clear();
var options = new List<string>();
foreach (MultilingualType t in Enum.GetValues(typeof(MultilingualType)))
{
if (t == MultilingualType.None || t == MultilingualType.Max) continue;
_exportLanguageOptions.Add(t);
options.Add(GetLanguageDisplayName(t));
}
if (ExportTargetLanguageDropdown != null)
{
ExportTargetLanguageDropdown.ClearOptions();
ExportTargetLanguageDropdown.AddOptions(options);
int idx = _exportLanguageOptions.IndexOf(MultilingualManager.Instance.CurrentType);
if (idx < 0) idx = _exportLanguageOptions.IndexOf(MultilingualType.EN);
if (idx < 0) idx = 0;
ExportTargetLanguageDropdown.SetValueWithoutNotify(idx);
}
if (ExportReferenceLanguageDropdown != null)
{
ExportReferenceLanguageDropdown.ClearOptions();
ExportReferenceLanguageDropdown.AddOptions(options);
int idx = _exportLanguageOptions.IndexOf(MultilingualType.ZH);
if (idx < 0) idx = 0;
ExportReferenceLanguageDropdown.SetValueWithoutNotify(idx);
}
}
// ExportCore核心文案待 MultilingualItem 加 IsCore 字段后实现)
private void OnExportCoreClicked()
{
SetExportStatus(ExportCoreStatusText, "核心文案导出尚未实现(需先在 MultilingualItem 上加 IsCore 标记)", success: false, useMultilingual: false);
}
// ExportFull全量导出 = WorkshopModExporter.ExportModTemplate跳过 IsDeprecated
// 用 coroutine 把"导出中→实际导出→导出成功"分两帧,让玩家看得到 InProgress 状态
private void OnExportFullClicked()
{
StopAllCoroutines();
StartCoroutine(CoExportFull());
}
private System.Collections.IEnumerator CoExportFull()
{
// 先显示"导出中",禁用按钮防止重复点
if (ExportFullButton != null) ExportFullButton.interactable = false;
if (ExportCoreButton != null) ExportCoreButton.interactable = false;
SetExportStatus(ExportFullStatusText, Table.Instance.TextDataAssets.OutsideModExportInProgress,
success: false, useMultilingual: true);
// 让 UI 渲染一帧(否则同步导出完成后 InProgress 文字根本来不及显示)
yield return null;
yield return null;
var (target, reference, modName) = ReadExportParams();
if (target == MultilingualType.None || target == MultilingualType.Max
|| reference == MultilingualType.None || reference == MultilingualType.Max)
{
SetExportStatus(ExportFullStatusText, "目标语言或参考语言无效", success: false, useMultilingual: false);
}
else
{
var path = WorkshopModExporter.ExportModTemplate(target, reference, modName);
if (string.IsNullOrEmpty(path))
{
SetExportStatus(ExportFullStatusText, "导出失败,请查看 Console 日志", success: false, useMultilingual: false);
}
else
{
_lastExportPath = path;
// 走多语言模板,{param} = 路径
var paramList = new List<string> { path };
if (ExportFullStatusText != null)
{
MultilingualManager.Instance.SetUIText(
ExportFullStatusText,
Table.Instance.TextDataAssets.OutsideModExportSucceeded,
paramList);
}
if (ExportOpenFolderButton != null) ExportOpenFolderButton.gameObject.SetActive(true);
// 导出成功后自动刷新上传子模块的"本地 mod 文件夹"下拉,让玩家立即能看到新导出的 mod
RefreshUploadFolderDropdown();
}
}
if (ExportFullButton != null) ExportFullButton.interactable = true;
if (ExportCoreButton != null) ExportCoreButton.interactable = true;
}
private (MultilingualType target, MultilingualType reference, string modName) ReadExportParams()
{
MultilingualType target = MultilingualType.EN;
MultilingualType reference = MultilingualType.ZH;
if (ExportTargetLanguageDropdown != null)
{
int i = ExportTargetLanguageDropdown.value;
if (i >= 0 && i < _exportLanguageOptions.Count) target = _exportLanguageOptions[i];
}
if (ExportReferenceLanguageDropdown != null)
{
int i = ExportReferenceLanguageDropdown.value;
if (i >= 0 && i < _exportLanguageOptions.Count) reference = _exportLanguageOptions[i];
}
string modName = ExportModNameInput != null ? ExportModNameInput.text?.Trim() : null;
if (string.IsNullOrEmpty(modName)) modName = null; // 让 Exporter 自动生成 TranslationMod_<lang>_<时间戳>
return (target, reference, modName);
}
// 在文件浏览器中打开最近一次导出目录
private void OnExportOpenFolderClicked()
{
if (string.IsNullOrEmpty(_lastExportPath)) return;
if (!System.IO.Directory.Exists(_lastExportPath))
{
// 目录已不存在:把错误显示在 Full 状态文本上(目前只有 Full 会写 _lastExportPath
SetExportStatus(ExportFullStatusText, "目录已不存在", success: false, useMultilingual: false);
return;
}
Application.OpenURL("file:///" + _lastExportPath.Replace('\\', '/'));
}
private void ResetExportStatus()
{
_lastExportPath = null;
// 进面板默认隐藏两个状态文本,避免占位
if (ExportCoreStatusText != null) ExportCoreStatusText.gameObject.SetActive(false);
if (ExportFullStatusText != null) ExportFullStatusText.gameObject.SetActive(false);
if (ExportOpenFolderButton != null) ExportOpenFolderButton.gameObject.SetActive(false);
}
// 设置某个按钮的状态文本。
// useMultilingual=true 时把 msg 视作多语言 ID用 SetUIText 渲染false 视作裸字符串
// 调试/错误信息走裸字符串;正式状态走多语言
// 调用此方法时会自动 SetActive(true),让默认隐藏的文本节点出现
private void SetExportStatus(TextMeshProUGUI target, string msg, bool success, bool useMultilingual)
{
if (target == null) return;
target.gameObject.SetActive(true);
if (useMultilingual)
{
MultilingualManager.Instance.SetUIText(target, msg);
}
else
{
// 强制清掉 MultilingualTextMono 的 ID否则下一次 ChangedMultilingual 会把裸字符串覆盖回多语言文本
var mono = target.GetComponent<Logic.Multilingual.MultilingualTextMono>();
if (mono != null) mono.ID = 0;
target.text = msg;
}
}
// ── MakeModePanel上传到 Workshop ──
// 进入 panel 时初始化上传模块(扫本地 mod 文件夹 + 设默认 toggle 状态)
private void InitUploadModule()
{
RefreshUploadFolderDropdown();
// 默认勾"创建新"
if (UploadCreateNewToggle != null)
{
UploadCreateNewToggle.SetIsOnWithoutNotify(true);
}
if (UploadUpdateExistingToggle != null)
{
UploadUpdateExistingToggle.SetIsOnWithoutNotify(false);
}
ApplyUploadModeUI();
if (UploadStatusText != null) UploadStatusText.text = "";
if (UploadOpenInSteamButton != null) UploadOpenInSteamButton.gameObject.SetActive(false);
if (UploadAgreementHint != null) UploadAgreementHint.SetActive(false);
RefreshUploadButtonInteractable();
}
// 扫 WorkshopMods/ 列出所有含 mod_info.json 的子目录
private void RefreshUploadFolderDropdown()
{
_uploadFolderOptions.Clear();
if (UploadModFolderDropdown == null) return;
UploadModFolderDropdown.ClearOptions();
var options = new List<string>();
foreach (var folder in WorkshopModLoader.GetLocalModPaths())
{
_uploadFolderOptions.Add(folder);
options.Add(System.IO.Path.GetFileName(folder));
}
if (options.Count == 0)
{
options.Add("(无可上传的本地 Mod)");
}
UploadModFolderDropdown.AddOptions(options);
UploadModFolderDropdown.SetValueWithoutNotify(0);
UploadModFolderDropdown.onValueChanged.RemoveAllListeners();
UploadModFolderDropdown.onValueChanged.AddListener(_ => OnUploadFolderChanged());
OnUploadFolderChanged();
}
// 选了某个本地 mod 文件夹后,自动从 mod_info.json 填充 标题/描述 + 检测预览图
private void OnUploadFolderChanged()
{
var folder = GetSelectedUploadFolder();
if (string.IsNullOrEmpty(folder))
{
if (UploadTitleInput != null) UploadTitleInput.text = "";
if (UploadDescriptionInput != null) UploadDescriptionInput.text = "";
if (UploadPreviewStatusText != null)
{
MultilingualManager.Instance.SetUIText(UploadPreviewStatusText,
Table.Instance.TextDataAssets.OutsideModUploadPreviewNotProvided);
}
RefreshUploadButtonInteractable();
return;
}
var info = WorkshopModExporter.ReadModInfo(folder);
if (UploadTitleInput != null)
{
UploadTitleInput.text = info != null ? (info.title ?? "") : System.IO.Path.GetFileName(folder);
}
if (UploadDescriptionInput != null)
{
UploadDescriptionInput.text = info != null ? (info.description ?? "") : "";
}
var previewPath = System.IO.Path.Combine(folder, WorkshopModExporter.PreviewFileName);
bool hasPreview = System.IO.File.Exists(previewPath);
if (UploadPreviewStatusText != null)
{
if (hasPreview)
{
// {param} = preview 文件名(让模板里可以写 "Detected: {param}" 等)
var paramList = new List<string> { WorkshopModExporter.PreviewFileName };
MultilingualManager.Instance.SetUIText(UploadPreviewStatusText,
Table.Instance.TextDataAssets.OutsideModUploadPreviewDetected, paramList);
}
else
{
MultilingualManager.Instance.SetUIText(UploadPreviewStatusText,
Table.Instance.TextDataAssets.OutsideModUploadPreviewNotProvided);
}
}
RefreshUploadButtonInteractable();
}
private string GetSelectedUploadFolder()
{
if (UploadModFolderDropdown == null) return null;
int i = UploadModFolderDropdown.value;
if (i < 0 || i >= _uploadFolderOptions.Count) return null;
return _uploadFolderOptions[i];
}
// 创建新/更新已有 切换:保证两个 toggle 互斥
private void OnUploadModeToggleChanged(bool _)
{
// Toggle group 的简单互斥:当一个被点亮,另一个置暗
if (UploadCreateNewToggle != null && UploadUpdateExistingToggle != null)
{
if (UploadCreateNewToggle.isOn && UploadUpdateExistingToggle.isOn)
{
// 后点的那个保持 on这里由 UI 自身行为决定。我们简单做:哪个被打开就关另一个
// 但 onValueChanged 没法区分谁是主动方,简化逻辑:始终保证至少一个开
// (依赖 Inspector 上把两个 toggle 配成同一个 ToggleGroup
}
if (!UploadCreateNewToggle.isOn && !UploadUpdateExistingToggle.isOn)
{
UploadCreateNewToggle.SetIsOnWithoutNotify(true);
}
}
ApplyUploadModeUI();
RefreshUploadButtonInteractable();
}
private void ApplyUploadModeUI()
{
bool isUpdate = UploadUpdateExistingToggle != null && UploadUpdateExistingToggle.isOn;
if (UploadUpdateTargetDropdown != null)
{
UploadUpdateTargetDropdown.gameObject.SetActive(isUpdate);
}
}
// 上传按钮可点性:必须有选中的本地 mod如果是 update 模式还得有选中的目标 fileId
private void RefreshUploadButtonInteractable()
{
if (UploadStartButton == null) return;
bool busy = WorkshopModUploader.Instance.IsBusy;
bool hasFolder = !string.IsNullOrEmpty(GetSelectedUploadFolder());
bool isUpdate = UploadUpdateExistingToggle != null && UploadUpdateExistingToggle.isOn;
bool hasUpdateTarget = !isUpdate || (_uploadUpdateTargets.Count > 0
&& UploadUpdateTargetDropdown != null
&& UploadUpdateTargetDropdown.value >= 0
&& UploadUpdateTargetDropdown.value < _uploadUpdateTargets.Count);
UploadStartButton.interactable = !busy && hasFolder && hasUpdateTarget;
}
// 用户已上传列表查询完成 → 同时刷新右侧 Update 目标下拉
private void OnUserModsQueryCompleted()
{
RefreshUploadUpdateTargetDropdown();
RefreshUploadedModList();
RefreshUploadButtonInteractable();
}
private void RefreshUploadUpdateTargetDropdown()
{
_uploadUpdateTargets.Clear();
if (UploadUpdateTargetDropdown == null) return;
UploadUpdateTargetDropdown.ClearOptions();
var options = new List<string>();
foreach (var item in WorkshopModBrowser.Instance.UserMods)
{
_uploadUpdateTargets.Add(item.FileId);
options.Add($"{item.Title} ({item.FileId.m_PublishedFileId})");
}
if (options.Count == 0)
{
options.Add("(暂无已上传的 Mod)");
}
UploadUpdateTargetDropdown.AddOptions(options);
UploadUpdateTargetDropdown.SetValueWithoutNotify(0);
UploadUpdateTargetDropdown.onValueChanged.RemoveAllListeners();
UploadUpdateTargetDropdown.onValueChanged.AddListener(_ => RefreshUploadButtonInteractable());
}
// 点击"上传"按钮:根据 toggle 走 CreateAndUpload 或 UpdateMod
private void OnUploadStartClicked()
{
var folder = GetSelectedUploadFolder();
if (string.IsNullOrEmpty(folder)) return;
var title = UploadTitleInput != null ? UploadTitleInput.text?.Trim() : "";
var description = UploadDescriptionInput != null ? UploadDescriptionInput.text ?? "" : "";
var previewPath = System.IO.Path.Combine(folder, WorkshopModExporter.PreviewFileName);
if (!System.IO.File.Exists(previewPath)) previewPath = null;
if (UploadAgreementHint != null) UploadAgreementHint.SetActive(false);
if (UploadOpenInSteamButton != null) UploadOpenInSteamButton.gameObject.SetActive(false);
_lastUploadedFileId = default;
bool isUpdate = UploadUpdateExistingToggle != null && UploadUpdateExistingToggle.isOn;
if (isUpdate)
{
int i = UploadUpdateTargetDropdown != null ? UploadUpdateTargetDropdown.value : -1;
if (i < 0 || i >= _uploadUpdateTargets.Count) return;
var fileId = _uploadUpdateTargets[i];
// 走 update 路径。Uploader 当前 UpdateMod 不接收 title/description
// title/description 用一次性的 SetItemTitle/SetItemDescription 绕过 → 简化版本:直接走 Update 流程
if (UploadStatusText != null) UploadStatusText.text = "正在更新...";
WorkshopModUploader.Instance.UpdateMod(fileId, folder, "Update from in-game",
success =>
{
OnUploadFinished(success, fileId);
});
}
else
{
if (UploadStatusText != null) UploadStatusText.text = "正在上传...";
WorkshopModUploader.Instance.CreateAndUploadMod(folder, title, description, previewPath,
(success, fileId) =>
{
OnUploadFinished(success, fileId);
});
}
RefreshUploadButtonInteractable();
}
private void OnUploadFinished(bool success, PublishedFileId_t fileId)
{
if (UploadStatusText != null) UploadStatusText.text = WorkshopModUploader.Instance.StatusMessage;
if (success)
{
_lastUploadedFileId = fileId;
if (UploadOpenInSteamButton != null) UploadOpenInSteamButton.gameObject.SetActive(true);
// 上传成功后刷新已上传列表,同时下拉的"更新目标"也会被刷新
QueryUserModsAndRefresh();
}
else
{
// 检查是否是协议未签
var msg = WorkshopModUploader.Instance.StatusMessage ?? "";
if (msg.Contains("需要同意协议=True") || msg.Contains("k_EResultAccessDenied"))
{
if (UploadAgreementHint != null) UploadAgreementHint.SetActive(true);
}
}
RefreshUploadButtonInteractable();
}
private void OnUploadOpenInSteamClicked()
{
if (_lastUploadedFileId.m_PublishedFileId == 0) return;
Application.OpenURL($"https://steamcommunity.com/sharedfiles/filedetails/?id={_lastUploadedFileId.m_PublishedFileId}");
}
private void OnUploadAgreementOpenClicked()
{
Application.OpenURL("https://steamcommunity.com/workshop/workshoplegalagreement/");
}
// ── MakeModePanel我已上传的 Mod 列表 ──
private void QueryUserModsAndRefresh()
{
if (UploadedModListStatusText != null)
{
UploadedModListStatusText.text = "正在查询...";
}
Logic.CrashSight.LogSystem.LogInfo("[UIOutsideMod] QueryUserMods 已发起");
WorkshopModBrowser.Instance.QueryUserMods();
}
private void OnUploadedModRefreshClicked()
{
Logic.CrashSight.LogSystem.LogInfo("[UIOutsideMod] 玩家点击刷新已上传列表");
QueryUserModsAndRefresh();
}
private void RefreshUploadedModList()
{
var browser = WorkshopModBrowser.Instance;
if (UploadedModListStatusText != null)
{
if (!string.IsNullOrEmpty(browser.UserQueryError))
{
UploadedModListStatusText.text = browser.UserQueryError;
}
else
{
UploadedModListStatusText.text = $"共 {browser.UserMods.Count} 项";
}
}
if (UploadedModListContent == null || UploadedModListItemPrefab == null) return;
// 池足量
while (_uploadedItemPool.Count < browser.UserMods.Count)
{
var go = Instantiate(UploadedModListItemPrefab, UploadedModListContent);
var mono = go.GetComponent<UIOutsideUploadedModListItemMono>();
if (mono == null) { Destroy(go); break; }
_uploadedItemPool.Add(mono);
}
for (int i = 0; i < _uploadedItemPool.Count; i++)
{
bool active = i < browser.UserMods.Count;
_uploadedItemPool[i].gameObject.SetActive(active);
if (!active) continue;
var mod = browser.UserMods[i];
_uploadedItemPool[i].SetContent(mod.FileId, mod.Title);
_uploadedItemPool[i].OnOpenInSteam = OnUploadedItemOpenInSteam;
}
}
private void OnUploadedItemOpenInSteam(PublishedFileId_t fileId)
{
Application.OpenURL($"https://steamcommunity.com/sharedfiles/filedetails/?id={fileId.m_PublishedFileId}");
}
}
}