File size: 10,073 Bytes
db14b86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
const TAB_PREFIX_LIST = ['txt2img', 'img2img'] as const;
const MODEL_TYPE_LIST = [
  'textual_inversion',
  'hypernetworks',
  'checkpoints',
  'lora',
  'lycoris',
] as const;
const MODEL_TYPE = {
  checkpoints: 'ckp',
  hypernetworks: 'hyper',
  lora: 'lora',
  lycoris: 'lycoris',
  textual_inversion: 'ti',
} as const satisfies Record<(typeof MODEL_TYPE_LIST)[number], string>;
const CARDID_SUFFIX = 'cards' as const;

// CSS
const BTN_MARGIN = '0';
const BTN_FONT_SIZE = '15px';
const BTN_THUMB_FONT_SIZE = '100%';
const BTN_THUMB_DISPLAY = 'inline';
const BTN_THUMB_POS = 'static';
const BTN_THUMB_BACKGROUND_IMAGE = 'none';
const BTN_THUMB_BACKGROUND = 'rgba(0, 0, 0, 0.8)';
const CH_BTN_TXTS = new Set(['🌐', 'πŸ’‘', '🏷️']);

const DOM_CACHE_PREFIX = 'lobe' as const;
const DOM_CACHE_KEY = `${DOM_CACHE_PREFIX}Done` as const;
const DOM_CACHE_VALUE = '1' as const;

const styleButton = (node: HTMLElement, isThumbMode: boolean) => {
  if (isThumbMode) {
    node.style.display = BTN_THUMB_DISPLAY;
    node.style.fontSize = BTN_THUMB_FONT_SIZE;
    node.style.position = BTN_THUMB_POS;
    node.style.backgroundImage = BTN_THUMB_BACKGROUND_IMAGE;
  } else {
    node.style.fontSize = BTN_FONT_SIZE;
    node.style.margin = BTN_MARGIN;
  }
};

type IStrictNullable<T> = T | null;
type INullable<T> = T | null | undefined;

function is_nullable<T>(v: INullable<T>): v is null | undefined {
  return v === undefined || v === null;
}

const updateCardForCivitai = () => {
  if (!document.querySelector('#tab_civitai_helper')) return;

  const replacePreviewText = getTranslation('replace preview') || 'replace preview';

  // Get component
  const chAlwaysDisplayCkb = document.querySelector(
    '#ch_always_display_ckb input',
  ) as HTMLInputElement;
  const chShowButtonOnThumbCkb = document.querySelector(
    '#ch_show_btn_on_thumb_ckb input',
  ) as HTMLInputElement;
  const chAlwaysDisplay = chAlwaysDisplayCkb?.checked || false;
  const chShowButtonOnThumb = chShowButtonOnThumbCkb?.checked || false;

  // Change all "replace preview" into an icon
  let extraNetworkId: `${(typeof TAB_PREFIX_LIST)[number]}_${(typeof MODEL_TYPE_LIST)[number]}_${typeof CARDID_SUFFIX}`;
  let extraNetworkNode: IStrictNullable<HTMLElement>;
  let metadataButton: IStrictNullable<HTMLElement>;
  let additionalNode: HTMLElement;
  let replacePreviewButton: IStrictNullable<HTMLElement>;
  let ulNode: IStrictNullable<HTMLElement>;
  let searchTermNode: IStrictNullable<HTMLElement>;
  let searchTerm = '';
  let modelType: (typeof MODEL_TYPE)[keyof typeof MODEL_TYPE];
  let cards: INullable<
    NodeListOf<
      HTMLElement & {
        dataset: {
          [DOM_CACHE_KEY]?: typeof DOM_CACHE_VALUE;
        };
      }
    >
  >;
  let needToAddButtons = false;
  let isThumbMode = false;

  const modelTypeHasCards: (typeof MODEL_TYPE_LIST)[number][] = [];

  // Get current tab
  for (const activeTabType of TAB_PREFIX_LIST) {
    for (const jsModelType of MODEL_TYPE_LIST) {
      modelType = MODEL_TYPE[jsModelType];
      // Get model_type for python side

      extraNetworkId = `${activeTabType}_${jsModelType}_${CARDID_SUFFIX}`;
      extraNetworkNode = document.querySelector(`#${extraNetworkId}` as const);

      // Check if extra network node exists
      if (is_nullable(extraNetworkNode)) continue;

      // Check if extr network is under thumbnail mode
      isThumbMode = extraNetworkNode.classList.contains('extra-network-thumbs');

      // Get all card nodes
      cards = extraNetworkNode.querySelectorAll('.card');
      const pending = !!document.querySelector(`#${extraNetworkId}_html .pending`);
      if (!cards?.length || pending) {
        if (!pending && extraNetworkNode.querySelector('.nocards')) {
          modelTypeHasCards.push(jsModelType);
        }

        continue;
      }

      modelTypeHasCards.push(jsModelType);

      for (const card of cards) {
        if (card.dataset[DOM_CACHE_KEY] === DOM_CACHE_VALUE) break;
        card.dataset[DOM_CACHE_KEY] = DOM_CACHE_VALUE;
        if (card.querySelectorAll('.actions .additional a').length > 2) continue;
        // Metadata_buttoncard
        metadataButton = card.querySelector('.metadata-button');
        // Additional node
        additionalNode = card.querySelector('.actions .additional')!;

        // Get ul node, which is the parent of all buttons
        ulNode = card.querySelector('.actions .additional ul');
        if (is_nullable(ulNode)) {
          ulNode = document.createElement('ul');
          additionalNode.append(ulNode);
        }

        // Replace preview text button
        replacePreviewButton = card.querySelector('.actions .additional a');
        if (is_nullable(replacePreviewButton)) {
          replacePreviewButton = document.createElement('a');
          additionalNode.append(replacePreviewButton);
        }

        // Remove br tag
        ulNode.querySelector('br')?.remove();

        // Check thumb mode
        if (isThumbMode && additionalNode) {
          additionalNode.style.display = undefined as any as string;

          if (chShowButtonOnThumb) {
            ulNode.style.background = BTN_THUMB_BACKGROUND;
          } else {
            // Reset
            ulNode.style.background = undefined as any as string;
            // Remove existed buttons

            // Find all .a child nodes
            const atags = ulNode.querySelectorAll('a');
            if (!atags?.length) continue;
            for (const atag of atags) {
              // Reset display
              atag.style.display = undefined as any;
              // Remove extension's button
              if (CH_BTN_TXTS.has(atag.innerHTML)) {
                // Need to remove
                atag.remove();
              } else {
                // Do not remove, just reset
                atag.innerHTML = replacePreviewText;
                atag.style.display = undefined as any;
                atag.style.fontSize = undefined as any;
                atag.style.position = undefined as any;
                atag.style.backgroundImage = undefined as any;
              }
            }

            // Just reset and remove nodes, do nothing else
            continue;
          }
        } else {
          // Full preview mode
          additionalNode.style.display = chAlwaysDisplay ? 'block' : (undefined as any as string);
        }

        // Change replace preview text button into icon
        if (replacePreviewButton.innerHTML !== 'πŸ–ΌοΈ') {
          needToAddButtons = true;
          replacePreviewButton.innerHTML = 'πŸ–ΌοΈ';
          styleButton(replacePreviewButton, isThumbMode);
        }

        if (!needToAddButtons) continue;
        // Search_term node
        // Search_term = subfolder path + model name + ext
        searchTermNode = card.querySelector('.actions .additional .search_term');
        if (!searchTermNode) return;
        // Get search_term
        searchTerm = searchTermNode.innerHTML;
        if (!searchTerm) continue;

        // Then we need to add 3 buttons to each ul node:
        const openUrlNode = document.createElement('a');
        openUrlNode.href = '#';
        openUrlNode.innerHTML = '🌐';
        styleButton(openUrlNode, isThumbMode);
        openUrlNode.title = "Open this model's civitai url";
        openUrlNode.setAttribute(
          'onclick',
          `open_model_url(event, '${modelType}', '${searchTerm}')`,
        );

        const addTriggerWordsNode = document.createElement('a');
        addTriggerWordsNode.href = '#';
        addTriggerWordsNode.innerHTML = 'πŸ’‘';
        styleButton(addTriggerWordsNode, isThumbMode);
        addTriggerWordsNode.title = 'Add trigger words to prompt';
        addTriggerWordsNode.setAttribute(
          'onclick',
          `add_trigger_words(event, '${modelType}', '${searchTerm}')`,
        );

        const usePreviewPromptNode = document.createElement('a');
        usePreviewPromptNode.href = '#';
        usePreviewPromptNode.innerHTML = '🏷️';
        styleButton(usePreviewPromptNode, isThumbMode);
        usePreviewPromptNode.title = 'Use prompt from preview image';
        usePreviewPromptNode.setAttribute(
          'onclick',
          `use_preview_prompt(event, '${modelType}', '${searchTerm}')`,
        );

        // Add to card
        ulNode.append(openUrlNode);
        // Add br if metadata_button exists
        if (isThumbMode && metadataButton) ulNode.append(document.createElement('br'));
        ulNode.append(addTriggerWordsNode);
        ulNode.append(usePreviewPromptNode);
      }
    }
  }

  return modelTypeHasCards;
};

export default () => {
  let checkDomCurrent: INullable<HTMLElement>;
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  let x: number = 0;
  let fn: () => any;
  // eslint-disable-next-line unicorn/consistent-function-scoping
  const fnClick = () => {
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    setTimeout(fn, 2000);
  };
  fn = () => {
    let retryTimes = 0;
    const fixInterval = setInterval(() => {
      console.debug('🀯 [civitai helper] update card for civitai');
      const checkDom = document.querySelector('#txt2img_lora_cards') as any;
      if (checkDom || retryTimes > 10) {
        if (checkDomCurrent !== checkDom) {
          x = 0;
          checkDomCurrent = checkDom;
          for (const activeTabType of TAB_PREFIX_LIST) {
            const elems = document.querySelectorAll(`#${activeTabType}_extra_tabs .tab-nav button`);
            if (elems) {
              for (const elem of elems) {
                elem.removeEventListener('click', fnClick);
                elem.addEventListener('click', fnClick);
              }
            }
          }
        }
        const y = updateCardForCivitai()?.length as number;
        if (typeof y === 'number' && y < x) x = y;
        if (retryTimes > 10 || !checkDom || y >= MODEL_TYPE_LIST.length || y > x) {
          clearInterval(fixInterval);
          x = y ?? x;
        }
      }
      retryTimes++;
    }, 2000);
  };

  return fn();
};