Some cleanup and new menu class.
This commit is contained in:
@@ -6,12 +6,15 @@ import {
|
||||
getClosestOrSelf,
|
||||
queryOne,
|
||||
query,
|
||||
setAttributes,
|
||||
} from "rgthree/common/utils_dom.js";
|
||||
import { logoCivitai, link, pencilColored, diskColored } from "rgthree/common/media/svgs.js";
|
||||
import { logoCivitai, link, pencilColored, diskColored, dotdotdot } from "rgthree/common/media/svgs.js";
|
||||
import { RgthreeModelInfo } from "typings/rgthree.js";
|
||||
import { SERVICE as MODEL_INFO_SERVICE } from "rgthree/common/model_info_service.js";
|
||||
import { rgthree } from "./rgthree.js";
|
||||
import { MenuButton } from "rgthree/common/menu.js";
|
||||
import { generateId } from "rgthree/common/shared_utils.js";
|
||||
import { rgthreeApi } from "rgthree/common/rgthree_api.js";
|
||||
|
||||
function injectCss(): Promise<void> {
|
||||
const href = "rgthree/common/css/dialog_model_info.css";
|
||||
@@ -152,20 +155,20 @@ export class RgthreeInfoDialog extends RgthreeDialog {
|
||||
const info = this.modelInfo || {};
|
||||
const civitaiLink = info.links?.find((i) => i.includes("civitai.com/models"));
|
||||
const html = `
|
||||
<ul class="rgthree-info-tags">
|
||||
<li title="Type" class="-type -type-${(info.type || "").toLowerCase()}"><span>${
|
||||
<ul class="rgthree-info-area">
|
||||
<li title="Type" class="rgthree-info-tag -type -type-${(info.type || "").toLowerCase()}"><span>${
|
||||
info.type || ""
|
||||
}</span></li>
|
||||
<li title="Base Model" class="-basemodel -basemodel-${(
|
||||
<li title="Base Model" class="rgthree-info-tag -basemodel -basemodel-${(
|
||||
info.baseModel || ""
|
||||
).toLowerCase()}"><span>${info.baseModel || ""}</span></li>
|
||||
<li stub="menu"></li>
|
||||
${
|
||||
!civitaiLink
|
||||
? ""
|
||||
: `
|
||||
<li title="Visit on Civitai" class="-link -civitai"><a href="${civitaiLink}" target="_blank">${logoCivitai} Civitai ${link}</a></li>
|
||||
`
|
||||
<li class="rgthree-info-menu" stub="menu"></li>
|
||||
${''
|
||||
// !civitaiLink
|
||||
// ? ""
|
||||
// : `
|
||||
// <li title="Visit on Civitai" class="-link -civitai"><a href="${civitaiLink}" target="_blank">Civitai ${link}</a></li>
|
||||
// `
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -188,14 +191,11 @@ export class RgthreeInfoDialog extends RgthreeDialog {
|
||||
: !info.raw?.civitai
|
||||
? infoTableRow(
|
||||
"Civitai",
|
||||
`<button data-action="fetch-civitai">Fetch info from civitai</button>`,
|
||||
`<button class="rgthree-button" data-action="fetch-civitai">Fetch info from civitai</button>`,
|
||||
)
|
||||
: ""
|
||||
}
|
||||
${infoTableRow(
|
||||
"Base Model",
|
||||
(info.baseModel || " ") + (info.baseModelFile ? `(${info.baseModelFile})` : ""),
|
||||
)}
|
||||
|
||||
${infoTableRow(
|
||||
"Name",
|
||||
info.name || info.raw?.metadata?.ss_output_name || "",
|
||||
@@ -203,11 +203,22 @@ export class RgthreeInfoDialog extends RgthreeDialog {
|
||||
"name",
|
||||
)}
|
||||
|
||||
${infoTableRow(
|
||||
${!info.baseModelFile && !info.baseModelFile ? '' : infoTableRow(
|
||||
"Base Model",
|
||||
(info.baseModel || "") + (info.baseModelFile ? ` (${info.baseModelFile})` : ""),
|
||||
)}
|
||||
|
||||
|
||||
${!info.trainedWords?.length ? '' : infoTableRow(
|
||||
"Trained Words",
|
||||
getTrainedWordsMarkup(info.trainedWords) ?? "",
|
||||
"Trained words from the metadata and/or civitai. Click to select for copy.",
|
||||
)}
|
||||
|
||||
${!info.raw?.metadata.ss_clip_skip || info.raw?.metadata.ss_clip_skip == 'None' ? '' : infoTableRow(
|
||||
"Clip Skip",
|
||||
info.raw?.metadata.ss_clip_skip
|
||||
)}
|
||||
${infoTableRow(
|
||||
"Strength Min",
|
||||
info.strengthMin ?? "",
|
||||
@@ -277,6 +288,37 @@ export class RgthreeInfoDialog extends RgthreeDialog {
|
||||
|
||||
const div = $el("div", { html });
|
||||
|
||||
if (rgthree.isDevMode()) {
|
||||
setAttributes(queryOne('[stub="menu"]', div)!, {
|
||||
children: [
|
||||
new MenuButton({
|
||||
icon: dotdotdot,
|
||||
options: [
|
||||
{ label: "More Actions", type: "title" },
|
||||
{
|
||||
label: "Open API JSON",
|
||||
callback: async (e: PointerEvent) => {
|
||||
if (this.modelInfo?.file) {
|
||||
window.open(`rgthree/api/loras/info?file=${encodeURIComponent(this.modelInfo.file)}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Clear all local info",
|
||||
callback: async (e: PointerEvent) => {
|
||||
if (this.modelInfo?.file) {
|
||||
this.modelInfo = await MODEL_INFO_SERVICE.clearLoraFetchedData(this.modelInfo.file);
|
||||
this.setContent(this.getInfoContent());
|
||||
this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown");
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
|
||||
@import '../common/css/buttons.scss';
|
||||
@import '../common/css/dialog.scss';
|
||||
@import '../common/css/menu.scss';
|
||||
|
||||
.rgthree-dialog.-settings {
|
||||
width: 100%;
|
||||
|
||||
@@ -990,6 +990,9 @@ class Rgthree extends EventTarget {
|
||||
}
|
||||
|
||||
isDevMode() {
|
||||
if (window.location.href.includes("#rgthree-dev=false")) {
|
||||
return false;
|
||||
}
|
||||
return GLOBAL_LOG_LEVEL >= LogLevel.DEBUG || window.location.href.includes("#rgthree-dev");
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,12 @@ export async function showLoraChooser(
|
||||
event: PointerEvent,
|
||||
callback: ContextMenuEventListener,
|
||||
parentMenu?: ContextMenu,
|
||||
loras?: string[],
|
||||
) {
|
||||
const canvas = app.canvas as TLGraphCanvas;
|
||||
const loras = ["None", ...(await rgthreeApi.getLoras())];
|
||||
if (!loras) {
|
||||
loras = ["None", ...(await rgthreeApi.getLoras())];
|
||||
}
|
||||
new LiteGraph.ContextMenu(loras, {
|
||||
event: event,
|
||||
parentMenu,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
width: 90vw;
|
||||
max-width: 960px;
|
||||
|
||||
.rgthree-info-tags {
|
||||
.rgthree-info-area {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -22,7 +22,8 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> * {
|
||||
&.rgthree-info-tag > * {
|
||||
min-height: 24px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
color: rgba(255,255,255,0.85);
|
||||
@@ -44,13 +45,8 @@
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:first-child {
|
||||
margin-right: .3em;
|
||||
display: none;
|
||||
}
|
||||
&:last-child {
|
||||
margin-left: .5em;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,20 +60,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.-civitai > * {
|
||||
color: #ddd;
|
||||
background: #1b65aa;
|
||||
transition: all 0.15s ease-in-out;
|
||||
&:hover {
|
||||
color: #fff;
|
||||
border-color: #1971c2;
|
||||
background: #1971c2;
|
||||
}
|
||||
}
|
||||
// &.-civitai > * {
|
||||
// color: #ddd;
|
||||
// background: #1b65aa;
|
||||
// transition: all 0.15s ease-in-out;
|
||||
// &:hover {
|
||||
// color: #fff;
|
||||
// border-color: #1971c2;
|
||||
// background: #1971c2;
|
||||
// }
|
||||
// }
|
||||
&.-type > * {
|
||||
background: rgb(73, 54, 94);
|
||||
color: rgb(228, 209, 248);
|
||||
}
|
||||
|
||||
&.rgthree-info-menu {
|
||||
margin-left: auto;
|
||||
|
||||
:not(#fakeid) & .rgthree-button {
|
||||
margin: 0;
|
||||
min-height: 24px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +159,7 @@
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
> input, > textarea{
|
||||
padding: 5px 10px;
|
||||
border: 0;
|
||||
@@ -162,6 +174,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:not(#fakeid) & .rgthree-button[data-action="fetch-civitai"] {
|
||||
font-size: inherit;
|
||||
padding: 6px 16px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
tr[data-field-name="userNote"] td > span:first-child {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
|
||||
.rgthree-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.08s ease-in-out;
|
||||
|
||||
color: #dde;
|
||||
background-color: #111;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 0 10px black !important;
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
padding: 4px 6px;
|
||||
z-index: 9999;
|
||||
white-space: nowrap;
|
||||
|
||||
&[role="button"] {
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
color: var(--input-text);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
filter: brightness(155%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[state^="measuring"] {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
}
|
||||
&[state="open"] {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { generateId, wait } from "./shared_utils.js";
|
||||
import { createElement as $el, getClosestOrSelf, setAttributes} from "./utils_dom.js";
|
||||
|
||||
/**
|
||||
* A menu. Mimics the comfy menu.
|
||||
*/
|
||||
class Menu {
|
||||
|
||||
private element: HTMLMenuElement = $el('menu.rgthree-menu');
|
||||
private callbacks: Map<string, (e: PointerEvent) => Promise<boolean|void>> = new Map();
|
||||
|
||||
private handleWindowPointerDownBound = this.handleWindowPointerDown.bind(this);
|
||||
|
||||
constructor(options: MenuOption[]) {
|
||||
this.setOptions(options);
|
||||
this.element.addEventListener('pointerup', async (e) => {
|
||||
const target = getClosestOrSelf(e.target as HTMLElement, "[data-callback],menu");
|
||||
if (e.which !== 1) {
|
||||
return;
|
||||
}
|
||||
const callback = target?.dataset?.['callback'];
|
||||
if (callback) {
|
||||
const halt = await this.callbacks.get(callback)?.(e);
|
||||
if (halt !== false) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(options: MenuOption[]) {
|
||||
for (const option of options) {
|
||||
if (option.type === 'title') {
|
||||
this.element.appendChild($el(`li`, {
|
||||
html: option.label
|
||||
}));
|
||||
} else {
|
||||
const id = generateId(8);
|
||||
this.callbacks.set(id, async (e: PointerEvent) => { return option?.callback?.(e); });
|
||||
this.element.appendChild($el(`li[role="button"][data-callback="${id}"]`, {
|
||||
html: option.label
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
async open(e: PointerEvent) {
|
||||
const parent = (e.target as HTMLElement).closest('div,dialog,body') as HTMLElement
|
||||
parent.appendChild(this.element);
|
||||
setAttributes(this.element, {
|
||||
style: {
|
||||
left: `${e.clientX + 16}px`,
|
||||
top: `${e.clientY - 16}px`,
|
||||
}
|
||||
});
|
||||
this.element.setAttribute('state', 'measuring-open');
|
||||
await wait(16);
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth) {
|
||||
this.element.style.left = `${e.clientX - rect.width - 16}px`;
|
||||
await wait(16);
|
||||
}
|
||||
this.element.setAttribute('state', 'open');
|
||||
setTimeout(() => {
|
||||
window.addEventListener('pointerdown', this.handleWindowPointerDownBound);
|
||||
});
|
||||
}
|
||||
|
||||
handleWindowPointerDown(e:PointerEvent) {
|
||||
if (!this.element.contains(e.target as HTMLElement)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
window.removeEventListener('pointerdown', this.handleWindowPointerDownBound);
|
||||
this.element.setAttribute('state', 'measuring-closed');
|
||||
await wait(16);
|
||||
this.element.setAttribute('state', 'closed');
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return (this.element.getAttribute('state') || '').includes('open');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type MenuOption = {
|
||||
label: string;
|
||||
type?: 'title'|'item'|'separator';
|
||||
callback?: (e: PointerEvent) => void;
|
||||
}
|
||||
|
||||
type MenuButtonOptions = {
|
||||
icon: string;
|
||||
options: MenuOption[];
|
||||
}
|
||||
|
||||
export class MenuButton {
|
||||
|
||||
private options: MenuButtonOptions;
|
||||
private menu: Menu;
|
||||
|
||||
private element: HTMLButtonElement = $el('button.rgthree-button[data-action="open-menu"]')
|
||||
|
||||
constructor(options: MenuButtonOptions) {
|
||||
this.options = options;
|
||||
this.element.innerHTML = options.icon;
|
||||
this.menu = new Menu(options.options);
|
||||
|
||||
this.element.addEventListener('pointerdown', (e) => {
|
||||
if (!this.menu.isOpen()) {
|
||||
this.menu.open(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toElement() {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class ModelInfoService {
|
||||
if (!refresh) {
|
||||
info = await rgthreeApi.getLoraInfo(file, light);
|
||||
} else {
|
||||
info = await rgthreeApi.refreshLoraInfo(file);
|
||||
info = await rgthreeApi.refreshLorasInfo(file);
|
||||
}
|
||||
if (!light) {
|
||||
this.loraToInfo.set(file, info);
|
||||
@@ -31,6 +31,12 @@ class ModelInfoService {
|
||||
return this.fetchLora(file, true);
|
||||
}
|
||||
|
||||
async clearLoraFetchedData(file: string) {
|
||||
await rgthreeApi.clearLorasInfo(file);
|
||||
this.loraToInfo.delete(file);
|
||||
return null;
|
||||
}
|
||||
|
||||
async saveLoraPartial(file: string, data: Partial<RgthreeModelInfo>) {
|
||||
let info = await rgthreeApi.saveLoraInfo(file, data);
|
||||
this.loraToInfo.set(file, info);
|
||||
|
||||
@@ -59,12 +59,6 @@ class RgthreeApi {
|
||||
);
|
||||
}
|
||||
|
||||
async refreshLoraInfo(lora: string): Promise<RgthreeModelInfo | null> {
|
||||
return await this.fetchApiJsonOrNull<RgthreeModelInfo>(
|
||||
`/loras/info/refresh?file=${encodeURIComponent(lora)}`,
|
||||
);
|
||||
}
|
||||
|
||||
async saveLoraInfo(
|
||||
lora: string,
|
||||
data: Partial<RgthreeModelInfo>,
|
||||
@@ -81,9 +75,20 @@ class RgthreeApi {
|
||||
return await this.fetchApiJsonOrNull<RgthreeModelInfo[]>(`/loras/info`);
|
||||
}
|
||||
|
||||
async refreshLorasInfo(): Promise<RgthreeModelInfo[] | null> {
|
||||
return await this.fetchApiJsonOrNull<RgthreeModelInfo[]>(`/loras/info/refresh`);
|
||||
async refreshLorasInfo(file: string): Promise<RgthreeModelInfo | null>;
|
||||
async refreshLorasInfo(): Promise<RgthreeModelInfo[] | null>;
|
||||
async refreshLorasInfo(file?: string) {
|
||||
const path = `/loras/info/refresh` + (file ? `?file=${encodeURIComponent(file)}` : '');
|
||||
const infos = await this.fetchApiJsonOrNull<RgthreeModelInfo[]|RgthreeModelInfo>(path);
|
||||
return infos;
|
||||
}
|
||||
|
||||
async clearLorasInfo(file?: string): Promise<void> {
|
||||
const path = `/loras/info/clear` + (file ? `?file=${encodeURIComponent(file)}` : '');
|
||||
await this.fetchApiJsonOrNull<RgthreeModelInfo[]>(path);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const rgthreeApi = new RgthreeApi();
|
||||
|
||||
Reference in New Issue
Block a user