All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

363 lines
11 KiB

  1. import { CORE_TEMPLATES } from '../scripts/mappingTemplates.js';
  2. import { TVA_CONFIG, updateSettings } from '../scripts/settings.js';
  3. import { BASE_IMAGE_CATEGORIES, uploadTokenImage } from '../scripts/utils.js';
  4. import { sortMappingsToGroups } from './effectMappingForm.js';
  5. import TokenCustomConfig from './tokenCustomConfig.js';
  6. // Edit overlay configuration as a json string
  7. export function showOverlayJsonConfigDialog(overlayConfig, callback) {
  8. const config = deepClone(overlayConfig || {});
  9. delete config.effect;
  10. let content = `<div style="height: 300px;" class="form-group stacked command"><textarea style="height: 300px;" class="configJson">${JSON.stringify(
  11. config,
  12. null,
  13. 2
  14. )}</textarea></div>`;
  15. new Dialog({
  16. title: `Overlay Configuration`,
  17. content: content,
  18. buttons: {
  19. yes: {
  20. icon: "<i class='fas fa-save'></i>",
  21. label: 'Save',
  22. callback: (html) => {
  23. let json = $(html).find('.configJson').val();
  24. if (json) {
  25. try {
  26. json = JSON.parse(json);
  27. } catch (e) {
  28. console.warn(`TVA |`, e);
  29. json = {};
  30. }
  31. } else {
  32. json = {};
  33. }
  34. callback(json);
  35. },
  36. },
  37. },
  38. default: 'yes',
  39. }).render(true);
  40. }
  41. // Change categories assigned to a path
  42. export async function showPathSelectCategoryDialog(event) {
  43. event.preventDefault();
  44. const typesInput = $(event.target).closest('.path-category').find('input');
  45. const selectedTypes = typesInput.val().split(',');
  46. const categories = BASE_IMAGE_CATEGORIES.concat(TVA_CONFIG.customImageCategories);
  47. let content = '<div class="token-variants-popup-settings">';
  48. // Split into rows of 4
  49. const splits = [];
  50. let currSplit = [];
  51. for (let i = 0; i < categories.length; i++) {
  52. if (i > 0 && i + 1 != categories.length && i % 4 == 0) {
  53. splits.push(currSplit);
  54. currSplit = [];
  55. }
  56. currSplit.push(categories[i]);
  57. }
  58. if (currSplit.length) splits.push(currSplit);
  59. for (const split of splits) {
  60. content += '<header class="table-header flexrow">';
  61. for (const type of split) {
  62. content += `<label>${type}</label>`;
  63. }
  64. content += '</header><ul class="setting-list"><li class="setting form-group"><div class="form-fields">';
  65. for (const type of split) {
  66. content += `<input class="category" type="checkbox" name="${type}" data-dtype="Boolean" ${
  67. selectedTypes.includes(type) ? 'checked' : ''
  68. }>`;
  69. }
  70. content += '</div></li></ul>';
  71. }
  72. content += '</div>';
  73. new Dialog({
  74. title: `Image Categories/Filters`,
  75. content: content,
  76. buttons: {
  77. yes: {
  78. icon: "<i class='fas fa-save'></i>",
  79. label: 'Apply',
  80. callback: (html) => {
  81. const types = [];
  82. $(html)
  83. .find('.category')
  84. .each(function () {
  85. if ($(this).is(':checked')) {
  86. types.push($(this).attr('name'));
  87. }
  88. });
  89. typesInput.val(types.join(','));
  90. },
  91. },
  92. },
  93. default: 'yes',
  94. }).render(true);
  95. }
  96. // Change configs assigned to a path
  97. export async function showPathSelectConfigForm(event) {
  98. event.preventDefault();
  99. const configInput = $(event.target).closest('.path-config').find('input');
  100. let config = {};
  101. try {
  102. config = JSON.parse(configInput.val());
  103. } catch (e) {}
  104. const setting = game.settings.get('core', DefaultTokenConfig.SETTING);
  105. const data = new foundry.data.PrototypeToken(setting);
  106. const token = new TokenDocument(data, { actor: null });
  107. new TokenCustomConfig(
  108. token,
  109. {},
  110. null,
  111. null,
  112. (conf) => {
  113. if (!conf) conf = {};
  114. if (conf.flags == null || isEmpty(conf.flags)) delete conf.flags;
  115. configInput.val(JSON.stringify(conf));
  116. const cog = configInput.siblings('.select-config');
  117. if (isEmpty(conf)) cog.removeClass('active');
  118. else cog.addClass('active');
  119. },
  120. config
  121. ).render(true);
  122. }
  123. export async function showTokenCaptureDialog(token) {
  124. if (!token) return;
  125. let content = `<form>
  126. <div class="form-group">
  127. <label>Image Name</label>
  128. <input type="text" name="name" value="${token.name}">
  129. </div>
  130. <div class="form-group">
  131. <label>Image Path</label>
  132. <div class="form-fields">
  133. <input type="text" name="path" value="modules/token-variants/">
  134. <button type="button" class="file-picker" data-type="folder" data-target="path" title="Browse Folders" tabindex="-1">
  135. <i class="fas fa-file-import fa-fw"></i>
  136. </button>
  137. </div>
  138. </div>
  139. <div class="form-group slim">
  140. <label>Width <span class="units">(pixels)</span></label>
  141. <div class="form-fields">
  142. <input type="number" step="1" name="width" value="${token.mesh.texture.width}">
  143. </div>
  144. </div>
  145. <div class="form-group slim">
  146. <label>Height <span class="units">(pixels)</span></label>
  147. <div class="form-fields">
  148. <input type="number" step="1" name="height" value="${token.mesh.texture.height}">
  149. </div>
  150. </div>
  151. <div class="form-group slim">
  152. <label>Scale</label>
  153. <div class="form-fields">
  154. <input type="number" step="any" name="scale" value="3">
  155. </div>
  156. </div>
  157. </form>`;
  158. new Dialog({
  159. title: `Save Token/Overlay Image`,
  160. content: content,
  161. buttons: {
  162. yes: {
  163. icon: "<i class='fas fa-save'></i>",
  164. label: 'Save',
  165. callback: (html) => {
  166. const options = {};
  167. $(html)
  168. .find('[name]')
  169. .each(function () {
  170. let val = parseFloat(this.value);
  171. if (isNaN(val)) val = this.value;
  172. options[this.name] = val;
  173. });
  174. uploadTokenImage(token, options);
  175. },
  176. },
  177. },
  178. render: (html) => {
  179. html.find('.file-picker').click(() => {
  180. new FilePicker({
  181. type: 'folder',
  182. current: html.find('[name="path"]').val(),
  183. callback: (path) => {
  184. html.find('[name="path"]').val(path);
  185. },
  186. }).render();
  187. });
  188. },
  189. default: 'yes',
  190. }).render(true);
  191. }
  192. export function showMappingSelectDialog(
  193. mappings,
  194. { title1 = 'Mappings', title2 = 'Select Mappings', buttonTitle = 'Confirm', callback = null } = {}
  195. ) {
  196. if (!mappings || !mappings.length) return;
  197. let content = `<form style="overflow-y: scroll; height:400px;"><h2>${title2}</h2>`;
  198. const [_, mappingGroups] = sortMappingsToGroups(mappings);
  199. for (const [group, obj] of Object.entries(mappingGroups)) {
  200. if (obj.list.length) {
  201. content += `<h4 style="text-align:center;"><b>${group}</b></h4>`;
  202. for (const mapping of obj.list) {
  203. content += `
  204. <div class="form-group">
  205. <label>${mapping.label}</label>
  206. <div class="form-fields">
  207. <input type="checkbox" name="${mapping.id}" data-dtype="Boolean">
  208. </div>
  209. </div>
  210. `;
  211. }
  212. }
  213. }
  214. content += `</form><div class="form-group"><button type="button" class="select-all">Select all</div>`;
  215. new Dialog({
  216. title: title1,
  217. content: content,
  218. buttons: {
  219. Ok: {
  220. label: buttonTitle,
  221. callback: async (html) => {
  222. if (!callback) return;
  223. const selectedMappings = [];
  224. html.find('input[type="checkbox"]').each(function () {
  225. if (this.checked) {
  226. const mapping = mappings.find((m) => m.id === this.name);
  227. if (mapping) {
  228. const cMapping = deepClone(mapping);
  229. selectedMappings.push(cMapping);
  230. delete cMapping.targetActors;
  231. }
  232. }
  233. });
  234. callback(selectedMappings);
  235. },
  236. },
  237. },
  238. render: (html) => {
  239. html.find('.select-all').click(() => {
  240. html.find('input[type="checkbox"]').prop('checked', true);
  241. });
  242. },
  243. }).render(true);
  244. }
  245. function showUserTemplateCreateDialog(mappings) {
  246. let content = `
  247. <div class="form-group">
  248. <label>Template Name</label>
  249. <div class="form-fields">
  250. <input type="text" name="templateName" data-dtype="String" value="">
  251. </div>
  252. </div>
  253. <div class="form-group">
  254. <label>Hover Text (optional)</label>
  255. <div class="form-fields">
  256. <input type="text" name="templateHint" data-dtype="String" value="">
  257. </div>
  258. </div>`;
  259. let dialog;
  260. dialog = new Dialog({
  261. title: 'Mapping Templates',
  262. content,
  263. buttons: {
  264. create: {
  265. label: 'Create Template',
  266. callback: (html) => {
  267. const name = html.find('[name="templateName"]').val();
  268. const hint = html.find('[name="templateHint"]').val();
  269. if (name.trim()) {
  270. TVA_CONFIG.templateMappings.push({ name, hint, mappings: deepClone(mappings) });
  271. updateSettings({ templateMappings: TVA_CONFIG.templateMappings });
  272. }
  273. },
  274. },
  275. cancel: {
  276. label: 'Cancel',
  277. },
  278. },
  279. default: 'cancel',
  280. });
  281. dialog.render(true);
  282. }
  283. export function showMappingTemplateDialog(mappings, callback) {
  284. let user_t = `<tr><th>USER Templates</th></tr>`;
  285. for (const template of TVA_CONFIG.templateMappings) {
  286. if (!template.id) template.id = randomID(8);
  287. user_t += `<tr draggable="true" data-id="${template.id}" title="${template.hint ?? ''}"><td class="template">${
  288. template.name
  289. }</td><td style="text-align:center;"><a class="delete-template"><i class="fa-solid fa-trash"></i></a></td></tr>`;
  290. }
  291. user_t = '<table>' + user_t + '</table>';
  292. user_t += `<button class="create-template" ${mappings.length ? '' : 'disabled'}>Create Template</button>'`;
  293. let core_t = `<tr><th><a href="https://github.com/Aedif/TokenVariants/wiki/Templates">CORE Templates</a></th></tr>`;
  294. for (const template of CORE_TEMPLATES) {
  295. if (!template.id) template.id = randomID(8);
  296. core_t += `<tr draggable="true" data-id="${template.id}" title="${template.hint ?? ''}"><td class="template">${
  297. template.name
  298. }</td></tr>`;
  299. }
  300. core_t = '<table>' + core_t + '</table>';
  301. let content =
  302. '<style>.template:hover {background-color: rgba(39, 245, 101, 0.55);}</style>' + user_t + '<hr>' + core_t;
  303. let dialog;
  304. dialog = new Dialog({
  305. title: 'Mapping Templates',
  306. content,
  307. buttons: {},
  308. render: (html) => {
  309. html.find('.template').on('click', (event) => {
  310. let id = $(event.target).closest('tr').data('id');
  311. if (id) {
  312. let template =
  313. CORE_TEMPLATES.find((t) => t.id === id) || TVA_CONFIG.templateMappings.find((t) => t.id === id);
  314. callback(template);
  315. }
  316. });
  317. html.find('.delete-template').on('click', async (event) => {
  318. const row = $(event.target).closest('tr');
  319. const id = row.data('id');
  320. if (id) {
  321. await updateSettings({
  322. templateMappings: TVA_CONFIG.templateMappings.filter((m) => m.id !== id),
  323. });
  324. row.remove();
  325. }
  326. });
  327. html.find('.create-template').on('click', () => {
  328. showMappingSelectDialog(mappings, {
  329. title1: 'Create Template',
  330. callback: (selectedMappings) => {
  331. if (selectedMappings.length) showUserTemplateCreateDialog(selectedMappings);
  332. },
  333. });
  334. dialog.close();
  335. });
  336. },
  337. });
  338. dialog.render(true);
  339. }