1578 lines
56 KiB
C#
1578 lines
56 KiB
C#
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_info,TargetLanguage 留空
|
||
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();
|
||
}
|
||
|
||
// 订阅状态 UI:3 个 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);
|
||
}
|
||
}
|
||
|
||
// ── SetLanguagePanel:Config 模式 ──
|
||
|
||
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:核心文案(跳过 IsSecondary 标记的次要文案)
|
||
private void OnExportCoreClicked()
|
||
{
|
||
StopAllCoroutines();
|
||
StartCoroutine(CoExport(coreOnly: true));
|
||
}
|
||
|
||
// ExportFull:全量导出(仅跳过 IsDeprecated)
|
||
// 用 coroutine 把"导出中→实际导出→导出成功"分两帧,让玩家看得到 InProgress 状态
|
||
private void OnExportFullClicked()
|
||
{
|
||
StopAllCoroutines();
|
||
StartCoroutine(CoExport(coreOnly: false));
|
||
}
|
||
|
||
private System.Collections.IEnumerator CoExport(bool coreOnly)
|
||
{
|
||
// 选用对应的状态文本(Core/Full 各显示在自己旁边)
|
||
var statusText = coreOnly ? ExportCoreStatusText : ExportFullStatusText;
|
||
|
||
// 先显示"导出中",禁用两个按钮防止并发/重复点
|
||
if (ExportFullButton != null) ExportFullButton.interactable = false;
|
||
if (ExportCoreButton != null) ExportCoreButton.interactable = false;
|
||
SetExportStatus(statusText, 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(statusText, "目标语言或参考语言无效", success: false, useMultilingual: false);
|
||
}
|
||
else
|
||
{
|
||
var path = WorkshopModExporter.ExportModTemplate(target, reference, modName, coreOnly);
|
||
if (string.IsNullOrEmpty(path))
|
||
{
|
||
SetExportStatus(statusText, "导出失败,请查看 Console 日志", success: false, useMultilingual: false);
|
||
}
|
||
else
|
||
{
|
||
_lastExportPath = path;
|
||
// 走多语言模板,{param} = 路径
|
||
var paramList = new List<string> { path };
|
||
if (statusText != null)
|
||
{
|
||
MultilingualManager.Instance.SetUIText(
|
||
statusText,
|
||
Table.Instance.TextDataAssets.OutsideModExportSucceeded,
|
||
paramList);
|
||
statusText.gameObject.SetActive(true);
|
||
}
|
||
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}");
|
||
}
|
||
}
|
||
}
|