Merge pull request #68 from space-nuko/code-editor
CodeMirror option for text widgets & Danbooru tag autocomplete
This commit is contained in:
14
package.json
14
package.json
@@ -42,6 +42,13 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.3.0",
|
||||
"@codemirror/commands": "^6.1.2",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.2.2",
|
||||
"@codemirror/state": "^6.1.2",
|
||||
"@codemirror/view": "^6.4.1",
|
||||
"@gradio/accordion": "workspace:*",
|
||||
"@gradio/atoms": "workspace:*",
|
||||
"@gradio/button": "workspace:*",
|
||||
@@ -67,6 +74,11 @@
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
"@zerodevx/svelte-json-view": "^1.0.5",
|
||||
"canvas-to-svg": "^1.0.3",
|
||||
"cm6-theme-basic-dark": "^0.2.0",
|
||||
"cm6-theme-basic-light": "^0.2.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"csv": "^6.3.0",
|
||||
"csv-parse": "^5.3.10",
|
||||
"events": "^3.3.0",
|
||||
"framework7": "^8.0.3",
|
||||
"framework7-svelte": "^8.0.3",
|
||||
@@ -74,7 +86,9 @@
|
||||
"klecks": "workspace:*",
|
||||
"pollen-css": "^4.6.2",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"style-mod": "^4.0.3",
|
||||
"svelte-bootstrap-icons": "^2.3.1",
|
||||
"svelte-codemirror-editor": "^1.1.0",
|
||||
"svelte-feather-icons": "^4.0.0",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte-select": "^5.5.3",
|
||||
|
||||
127
pnpm-lock.yaml
generated
127
pnpm-lock.yaml
generated
@@ -4,6 +4,27 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@codemirror/autocomplete':
|
||||
specifier: ^6.3.0
|
||||
version: 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.1.2
|
||||
version: 6.2.4
|
||||
'@codemirror/language':
|
||||
specifier: ^6.6.0
|
||||
version: 6.6.0
|
||||
'@codemirror/lint':
|
||||
specifier: ^6.0.0
|
||||
version: 6.2.1
|
||||
'@codemirror/search':
|
||||
specifier: ^6.2.2
|
||||
version: 6.4.0
|
||||
'@codemirror/state':
|
||||
specifier: ^6.1.2
|
||||
version: 6.2.0
|
||||
'@codemirror/view':
|
||||
specifier: ^6.4.1
|
||||
version: 6.11.0
|
||||
'@gradio/accordion':
|
||||
specifier: workspace:*
|
||||
version: link:gradio/js/accordion
|
||||
@@ -79,6 +100,21 @@ importers:
|
||||
canvas-to-svg:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
cm6-theme-basic-dark:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
|
||||
cm6-theme-basic-light:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
|
||||
codemirror:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
csv:
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0
|
||||
csv-parse:
|
||||
specifier: ^5.3.10
|
||||
version: 5.3.10
|
||||
events:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
@@ -100,9 +136,15 @@ importers:
|
||||
radix-icons-svelte:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
style-mod:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
svelte-bootstrap-icons:
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
svelte-codemirror-editor:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(codemirror@6.0.1)
|
||||
svelte-feather-icons:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -1341,6 +1383,19 @@ packages:
|
||||
commander: 2.20.3
|
||||
dev: false
|
||||
|
||||
/@codemirror/autocomplete@6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0):
|
||||
resolution: {integrity: sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
dependencies:
|
||||
'@codemirror/language': 6.6.0
|
||||
'@codemirror/state': 6.2.0
|
||||
'@codemirror/view': 6.11.0
|
||||
'@lezer/common': 1.0.2
|
||||
dev: false
|
||||
|
||||
/@codemirror/autocomplete@6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2):
|
||||
resolution: {integrity: sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==}
|
||||
peerDependencies:
|
||||
@@ -3872,6 +3927,19 @@ packages:
|
||||
engines: {node: '>=0.8'}
|
||||
dev: false
|
||||
|
||||
/cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0):
|
||||
resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
'@lezer/highlight': ^1.0.0
|
||||
dependencies:
|
||||
'@codemirror/language': 6.6.0
|
||||
'@codemirror/state': 6.2.0
|
||||
'@codemirror/view': 6.11.0
|
||||
dev: false
|
||||
|
||||
/cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4):
|
||||
resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==}
|
||||
peerDependencies:
|
||||
@@ -3886,6 +3954,19 @@ packages:
|
||||
'@lezer/highlight': 1.1.4
|
||||
dev: false
|
||||
|
||||
/cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0):
|
||||
resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
'@lezer/highlight': ^1.0.0
|
||||
dependencies:
|
||||
'@codemirror/language': 6.6.0
|
||||
'@codemirror/state': 6.2.0
|
||||
'@codemirror/view': 6.11.0
|
||||
dev: false
|
||||
|
||||
/cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4):
|
||||
resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==}
|
||||
peerDependencies:
|
||||
@@ -3909,6 +3990,18 @@ packages:
|
||||
resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==}
|
||||
dev: false
|
||||
|
||||
/codemirror@6.0.1:
|
||||
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)
|
||||
'@codemirror/commands': 6.2.4
|
||||
'@codemirror/language': 6.6.0
|
||||
'@codemirror/lint': 6.2.1
|
||||
'@codemirror/search': 6.4.0
|
||||
'@codemirror/state': 6.2.0
|
||||
'@codemirror/view': 6.11.0
|
||||
dev: false
|
||||
|
||||
/codemirror@6.0.1(@lezer/common@1.0.2):
|
||||
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
|
||||
dependencies:
|
||||
@@ -4133,6 +4226,28 @@ packages:
|
||||
rrweb-cssom: 0.6.0
|
||||
dev: true
|
||||
|
||||
/csv-generate@4.2.6:
|
||||
resolution: {integrity: sha512-VtnYqhWLcsUocA346ewFOk+rrqcoT663j11vXzD2uelXq9WguQ3QzDeVD8ISso7hhVtkDSHcWl9psdemeiEHDA==}
|
||||
dev: false
|
||||
|
||||
/csv-parse@5.3.10:
|
||||
resolution: {integrity: sha512-cTXY6iy0gN5Ha/cGILeDgQE+nKiKDU2m0DjSRdJhr86BN3cM7oefBsTk2aH0LQeaYtL7Z7HvW+jYoadmdhzeDA==}
|
||||
dev: false
|
||||
|
||||
/csv-stringify@6.4.0:
|
||||
resolution: {integrity: sha512-HQsw0QXiN5fdlO+R8/JzCZnR3Fqp8E87YVnhHlaPtNGJjt6ffbV0LpOkieIb1x6V1+xt878IYq77SpXHWAqKkA==}
|
||||
dev: false
|
||||
|
||||
/csv@6.3.0:
|
||||
resolution: {integrity: sha512-wXakaMNIz6qe/a15AXQ6JA9urN2lEx08J/3xpxv7Ke3GOrio300Ikbqzrlg1ML0fuEOULHhswvSxtN5h/72sHg==}
|
||||
engines: {node: '>= 0.1.90'}
|
||||
dependencies:
|
||||
csv-generate: 4.2.6
|
||||
csv-parse: 5.3.10
|
||||
csv-stringify: 6.4.0
|
||||
stream-transform: 3.2.6
|
||||
dev: false
|
||||
|
||||
/d3-array@3.2.2:
|
||||
resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7801,6 +7916,10 @@ packages:
|
||||
/std-env@3.3.3:
|
||||
resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==}
|
||||
|
||||
/stream-transform@3.2.6:
|
||||
resolution: {integrity: sha512-/pyOvaCQFqYTmrFhmMbnAEVo3SsTx1H39eUVPOtYeAgbEUc+rDo7GoP8LbHJgU83mKtzJe/7Nq/ipaAnUOHgJQ==}
|
||||
dev: false
|
||||
|
||||
/streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -7978,6 +8097,14 @@ packages:
|
||||
- sugarss
|
||||
dev: true
|
||||
|
||||
/svelte-codemirror-editor@1.1.0(codemirror@6.0.1):
|
||||
resolution: {integrity: sha512-wFdMIsZds5qzn3x2NbFUxDVU6Cn3rwFdq0035ypaFVgzTjJ90bnPm6IbrFA4OJz1ngIyfbIuPAPDjm7rJIr0gg==}
|
||||
peerDependencies:
|
||||
codemirror: ^6.0.0
|
||||
dependencies:
|
||||
codemirror: 6.0.1
|
||||
dev: false
|
||||
|
||||
/svelte-dnd-action@0.9.22(svelte@3.58.0):
|
||||
resolution: {integrity: sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==}
|
||||
peerDependencies:
|
||||
|
||||
100000
public/extra/danbooru.csv
Normal file
100000
public/extra/danbooru.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import { download } from "./utils";
|
||||
* components they represent in the UI.
|
||||
*/
|
||||
export type ComfyBoxTemplate = {
|
||||
version: 1,
|
||||
nodes: LGraphNode[],
|
||||
links: LLink[],
|
||||
container?: DragItemEntry
|
||||
@@ -20,6 +21,8 @@ export type ComfyBoxTemplate = {
|
||||
* components they represent in the UI.
|
||||
*/
|
||||
export type SerializedComfyBoxTemplate = {
|
||||
version: 1,
|
||||
|
||||
/*
|
||||
* Serialized nodes
|
||||
*/
|
||||
@@ -270,6 +273,7 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
|
||||
[nodes, links] = pruneDetachedLinks(nodes, links);
|
||||
|
||||
let comfyBoxTemplate: SerializedComfyBoxTemplate = {
|
||||
version: 1,
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
layout: layout
|
||||
@@ -320,6 +324,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
container: container
|
||||
@@ -328,6 +333,7 @@ export function createTemplate(nodes: LGraphNode[]): ComfyBoxTemplateResult {
|
||||
else {
|
||||
// No UI to serialize.
|
||||
return {
|
||||
version: 1,
|
||||
nodes: nodes,
|
||||
links: links,
|
||||
}
|
||||
|
||||
198
src/lib/DanbooruTags.ts
Normal file
198
src/lib/DanbooruTags.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { parse } from 'csv-parse/browser/esm/sync';
|
||||
import { timeExecutionMs } from './utils';
|
||||
import { insertCompletionText, type Completion, type CompletionContext, type CompletionResult, type CompletionSource, type CompletionConfig, autocompletion } from '@codemirror/autocomplete';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Extension, TransactionSpec } from '@codemirror/state';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { StyleSpec } from "style-mod"
|
||||
|
||||
export enum DanbooruTagCategory {
|
||||
General = 0,
|
||||
Artist = 1,
|
||||
Copyright = 3,
|
||||
Character = 4,
|
||||
}
|
||||
|
||||
export type DanbooruTagCategoryData = {
|
||||
name: string,
|
||||
color: string
|
||||
}
|
||||
|
||||
const TAG_CATEGORY_DATA: Record<DanbooruTagCategory, DanbooruTagCategoryData> = {
|
||||
[DanbooruTagCategory.General]: {
|
||||
name: "general",
|
||||
color: "lightblue"
|
||||
},
|
||||
[DanbooruTagCategory.Artist]: {
|
||||
name: "artist",
|
||||
color: "red",
|
||||
},
|
||||
[DanbooruTagCategory.Copyright]: {
|
||||
name: "copyright",
|
||||
color: "lightpurple"
|
||||
},
|
||||
[DanbooruTagCategory.Character]: {
|
||||
name: "character",
|
||||
color: "green"
|
||||
}
|
||||
}
|
||||
|
||||
export const TAG_CATEGORY_COLORS: StyleSpec = Object.values(TAG_CATEGORY_DATA)
|
||||
.flatMap(d => {
|
||||
return [
|
||||
[`.cm-autocompletion-${d.name}`, { color: d.color + " !important" }],
|
||||
]
|
||||
})
|
||||
.reduce((dict, el) => (dict[el[0]] = el[1], dict), {})
|
||||
|
||||
export type DanbooruTag = {
|
||||
text: string,
|
||||
category: DanbooruTagCategory,
|
||||
count: number,
|
||||
aliases: string[]
|
||||
}
|
||||
|
||||
function escapeRegExp(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function formatPostCount(postCount: number): string {
|
||||
if (!postCount || !isNaN(postCount))
|
||||
return ""
|
||||
|
||||
let formatter: Intl.NumberFormat;
|
||||
|
||||
// Danbooru formats numbers with a padded fraction for 1M or 1k, but not for 10/100k
|
||||
if (postCount >= 1000000 || (postCount >= 1000 && postCount < 10000))
|
||||
formatter = Intl.NumberFormat("en", { notation: "compact", minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||||
else
|
||||
formatter = Intl.NumberFormat("en", { notation: "compact" });
|
||||
|
||||
return formatter.format(postCount);
|
||||
}
|
||||
|
||||
export default class DanbooruTags {
|
||||
private static _instance: DanbooruTags;
|
||||
|
||||
tags: DanbooruTag[] = [];
|
||||
tagsByCategory: Record<string, DanbooruTag> = {}
|
||||
aliases: Record<string, number> = {}
|
||||
|
||||
static get instance(): DanbooruTags {
|
||||
if (!DanbooruTags._instance)
|
||||
DanbooruTags._instance = new DanbooruTags()
|
||||
return DanbooruTags._instance
|
||||
}
|
||||
|
||||
async load(force: boolean = false) {
|
||||
console.log("Parsing danbooru tags CSV...")
|
||||
|
||||
if (this.tags.length > 0 && !force) {
|
||||
console.info("Danbooru tags already parsed")
|
||||
return;
|
||||
}
|
||||
|
||||
this.tags = []
|
||||
|
||||
const transformTag = (data: string[]): DanbooruTag => {
|
||||
return {
|
||||
text: data[0],
|
||||
category: parseInt(data[1]),
|
||||
count: parseInt(data[2]),
|
||||
aliases: data[3].split(",")
|
||||
}
|
||||
}
|
||||
|
||||
const time = await timeExecutionMs(async () => {
|
||||
const resp = await fetch("/extra/danbooru.csv");
|
||||
const csv = await resp.text();
|
||||
const raw: string[][] = parse(csv, {
|
||||
delimiter: ','
|
||||
});
|
||||
|
||||
const refined = raw.map(transformTag);
|
||||
|
||||
this.tags = refined
|
||||
})
|
||||
|
||||
console.log(`Parsed ${this.tags.length} tags in ${time / 1000}ms.`)
|
||||
console.error(this.tags[0])
|
||||
}
|
||||
|
||||
autocomplete(context: CompletionContext): CompletionResult {
|
||||
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
|
||||
let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
|
||||
|
||||
let weightBefore = /:[0-9.]+$/.exec(textBefore)
|
||||
if (weightBefore) return null
|
||||
|
||||
let tagBefore = /\b[a-zA-Z0-9_()-]+$/.exec(textBefore)
|
||||
if (!tagBefore) return null
|
||||
|
||||
let tagword = tagBefore[0]
|
||||
console.warn(tagword)
|
||||
|
||||
let searchRegex: RegExp;
|
||||
if (tagword.startsWith("*")) {
|
||||
tagword = tagword.slice(1);
|
||||
searchRegex = new RegExp(`${escapeRegExp(tagword)}`, 'i');
|
||||
} else {
|
||||
searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i');
|
||||
}
|
||||
|
||||
const sanitize = (rawTag: string): string => {
|
||||
let sanitized = rawTag.replaceAll("_", " ");
|
||||
|
||||
// TODO config
|
||||
const escapeParentheses = true;
|
||||
const isTagType = true;
|
||||
if (escapeParentheses && isTagType) {
|
||||
sanitized = sanitized
|
||||
.replaceAll("(", "\\(")
|
||||
.replaceAll(")", "\\)")
|
||||
.replaceAll("[", "\\[")
|
||||
.replaceAll("]", "\\]");
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
const apply = (view: EditorView, completion: Completion, from: number, to: number) => {
|
||||
const sanitized = sanitize(completion.label);
|
||||
view.dispatch(insertCompletionText(view.state, sanitized, from, to));
|
||||
}
|
||||
|
||||
const filter = (x: DanbooruTag) => x.text.toLowerCase().search(searchRegex) > -1;
|
||||
|
||||
const options: Completion[] = this.tags.filter(filter).map(t => {
|
||||
const categoryName = TAG_CATEGORY_DATA[t.category]?.name || "unknown";
|
||||
return {
|
||||
label: t.text,
|
||||
apply,
|
||||
detail: formatPostCount(t.count),
|
||||
type: categoryName,
|
||||
section: "Tags"
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
|
||||
options,
|
||||
validFor: /^\b([\w_()-]+)?$/
|
||||
}
|
||||
}
|
||||
|
||||
static getCompletionExt(): Extension {
|
||||
const source: CompletionSource = DanbooruTags.instance.autocomplete.bind(DanbooruTags.instance)
|
||||
|
||||
const optionClass = (completion: Completion): string => {
|
||||
return `cm-autocompletion-${completion.type}`
|
||||
}
|
||||
|
||||
return autocompletion({
|
||||
override: [source],
|
||||
interactionDelay: 250,
|
||||
optionClass
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export class ImageViewer {
|
||||
currentImages: string[] = []
|
||||
selectedIndex: number = -1;
|
||||
currentGallery: HTMLDivElement | null = null;
|
||||
static _instance: ImageViewer;
|
||||
private static _instance: ImageViewer;
|
||||
|
||||
static get instance(): ImageViewer {
|
||||
if (!ImageViewer._instance)
|
||||
|
||||
@@ -34,6 +34,7 @@ import { tick } from "svelte";
|
||||
import { type SvelteComponentDev } from "svelte/internal";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import ComfyPromptSerializer, { isActiveBackendNode, UpstreamNodeLocator } from "./ComfyPromptSerializer";
|
||||
import DanbooruTags from "$lib/DanbooruTags";
|
||||
|
||||
export const COMFYBOX_SERIAL_VERSION = 1;
|
||||
|
||||
@@ -232,6 +233,8 @@ export default class ComfyApp {
|
||||
|
||||
await this.updateHistoryAndQueue();
|
||||
|
||||
await this.initFrontendFeatures();
|
||||
|
||||
// await this.#invokeExtensionsAsync("setup");
|
||||
|
||||
// Ensure the canvas fills the window
|
||||
@@ -586,6 +589,10 @@ export default class ComfyApp {
|
||||
});
|
||||
}
|
||||
|
||||
private async initFrontendFeatures() {
|
||||
await DanbooruTags.instance.load();
|
||||
}
|
||||
|
||||
private async updateHistoryAndQueue() {
|
||||
const queue = await this.api.getQueue();
|
||||
const history = await this.api.getHistory();
|
||||
|
||||
@@ -3,8 +3,9 @@ import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
|
||||
import layoutStates from './stores/layoutStates';
|
||||
import { get } from 'svelte/store';
|
||||
import workflowState from './stores/workflowState';
|
||||
import DanbooruTags from './DanbooruTags';
|
||||
|
||||
export function configureLitegraph(isMobile: boolean = false) {
|
||||
function configureLitegraph(isMobile: boolean = false) {
|
||||
LiteGraph.catch_exceptions = false;
|
||||
|
||||
// Must be enabled, otherwise subgraphs won't work (because of non-unique node/link IDs)
|
||||
@@ -26,10 +27,18 @@ export function configureLitegraph(isMobile: boolean = false) {
|
||||
}
|
||||
|
||||
Subgraph.default_lgraph_factory = () => new ComfyGraph;
|
||||
|
||||
(window as any).LiteGraph = LiteGraph;
|
||||
(window as any).LGraphCanvas = LGraphCanvas;
|
||||
(window as any).layoutStates = layoutStates;
|
||||
(window as any).workflowState = workflowState;
|
||||
(window as any).svelteGet = get;
|
||||
}
|
||||
|
||||
function configureGlobals() {
|
||||
const win = window as any
|
||||
win.LiteGraph = LiteGraph;
|
||||
win.LGraphCanvas = LGraphCanvas;
|
||||
win.layoutStates = layoutStates;
|
||||
win.workflowState = workflowState;
|
||||
win.svelteGet = get;
|
||||
}
|
||||
|
||||
export default function init(isMobile: boolean = false) {
|
||||
configureLitegraph(isMobile);
|
||||
configureGlobals();
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
defaultValue: ""
|
||||
},
|
||||
|
||||
// Editor
|
||||
// Image Editor
|
||||
{
|
||||
name: "variant",
|
||||
type: "enum",
|
||||
@@ -430,6 +430,16 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
|
||||
},
|
||||
|
||||
// Text
|
||||
{
|
||||
name: "variant",
|
||||
type: "enum",
|
||||
location: "widget",
|
||||
editable: true,
|
||||
validNodeTypes: ["ui/text"],
|
||||
values: ["text", "code"],
|
||||
defaultValue: "text",
|
||||
refreshPanelOnChange: true
|
||||
},
|
||||
{
|
||||
name: "multiline",
|
||||
type: "boolean",
|
||||
|
||||
@@ -52,6 +52,14 @@ export function* enumerate<T>(iterable: Iterable<T>): Iterable<[number, T]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function timeExecutionMs(fn: (...any) => Promise<any>, ...args: any[]): Promise<number> {
|
||||
const start = new Date().getTime();
|
||||
|
||||
await fn.apply(null, args)
|
||||
|
||||
return new Date().getTime() - start;
|
||||
}
|
||||
|
||||
export function download(filename: string, text: string, type: string = "text/plain") {
|
||||
const blob = new Blob([text], { type: type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||
export let widget: WidgetLayout | null = null;
|
||||
export let isMobile: boolean = false;
|
||||
import TextWidgetCodeVariant from "./TextWidgetCodeVariant.svelte"
|
||||
|
||||
let node: ComfyTextNode | null = null;
|
||||
let nodeValue: Writable<string> | null = null;
|
||||
@@ -31,6 +32,9 @@
|
||||
|
||||
<div class="wrapper gradio-textbox">
|
||||
{#if node !== null && nodeValue !== null}
|
||||
{#if widget.attrs.variant === "code"}
|
||||
<TextWidgetCodeVariant {widget} {node} {nodeValue} />
|
||||
{:else}
|
||||
<TextBox
|
||||
bind:value={$nodeValue}
|
||||
label={widget.attrs.title}
|
||||
@@ -44,6 +48,7 @@
|
||||
on:select
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
233
src/lib/widgets/TextWidgetCodeVariant.svelte
Normal file
233
src/lib/widgets/TextWidgetCodeVariant.svelte
Normal file
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import type { ComfyTextNode } from "$lib/nodes/widgets";
|
||||
import { type WidgetLayout } from "$lib/stores/layoutStates";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
import type { ViewUpdate } from "@codemirror/view";
|
||||
import { EditorView, keymap, placeholder as placeholderExt } from "@codemirror/view";
|
||||
import { StateEffect, EditorState, type Extension } from "@codemirror/state";
|
||||
import { basicDark } from "cm6-theme-basic-dark";
|
||||
import { basicLight } from "cm6-theme-basic-light";
|
||||
|
||||
import { basicSetup } from "./TextWidgetCodeVariant";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { TAG_CATEGORY_COLORS } from "$lib/DanbooruTags";
|
||||
|
||||
export let widget: WidgetLayout;
|
||||
export let node: ComfyTextNode;
|
||||
export let nodeValue: Writable<string> = writable("");
|
||||
export let extraExtensions: Extension[] = [];
|
||||
let lines = 5;
|
||||
let classNames = ""
|
||||
|
||||
let element: HTMLDivElement;
|
||||
let view: EditorView;
|
||||
|
||||
const dispatch = createEventDispatcher<{ change: string }>();
|
||||
|
||||
$: lines = node?.properties?.lines || 5;
|
||||
|
||||
let BaseTheme: Extension = EditorView.theme({
|
||||
"&": {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
height: "12rem",
|
||||
fontSize: "var(--text-sm)",
|
||||
backgroundColor: "var(--input-background-fill)"
|
||||
},
|
||||
".cm-content": {
|
||||
paddingTop: "5px",
|
||||
paddingBottom: "5px",
|
||||
color: "var(--body-text-color)",
|
||||
fontFamily: "var(--font-mono)",
|
||||
minHeight: "100%"
|
||||
},
|
||||
".cm-gutters": {
|
||||
marginRight: "1px",
|
||||
borderRight: "1px solid var(--border-color-primary)",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--body-text-color-subdued)"
|
||||
},
|
||||
".cm-focused": {
|
||||
outline: "none"
|
||||
},
|
||||
".cm-scroller": {
|
||||
height: "auto"
|
||||
},
|
||||
".cm-cursor": {
|
||||
borderLeftColor: "var(--body-text-color)"
|
||||
},
|
||||
".cm-selectionBackground": {
|
||||
backgroundColor: "var(--secondary-600) !important",
|
||||
},
|
||||
".cm-tooltip": {
|
||||
backgroundColor: "var(--panel-background-fill) !important",
|
||||
border: "1px solid var(--panel-border-color) !important",
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
color: "var(--body-text-color) !important",
|
||||
},
|
||||
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
|
||||
color: "unset"
|
||||
},
|
||||
...TAG_CATEGORY_COLORS
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
view = createEditorView();
|
||||
return () => view?.destroy();
|
||||
});
|
||||
|
||||
$: reconfigure()
|
||||
$: setDoc($nodeValue);
|
||||
$: updateLines(lines);
|
||||
|
||||
function reconfigure(): void {
|
||||
view?.dispatch({
|
||||
effects: StateEffect.reconfigure.of(getExtensions())
|
||||
});
|
||||
}
|
||||
|
||||
function setDoc(newDoc: string) {
|
||||
if (view && newDoc !== view.state.doc.toString()) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: newDoc
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateLines(newLines: number) {
|
||||
if (view) {
|
||||
view.requestMeasure({ read: updateGutters });
|
||||
}
|
||||
}
|
||||
|
||||
function getGutterLineHeight(view: EditorView): string | null {
|
||||
let elements = view.dom.querySelectorAll<HTMLElement>(".cm-gutterElement");
|
||||
if (elements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
let node = elements[i];
|
||||
let height = getComputedStyle(node)?.height ?? "0px";
|
||||
if (height != "0px") {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateGutters(view: EditorView): any {
|
||||
let gutters = view.dom.querySelectorAll<HTMLElement>(".cm-gutter");
|
||||
let _lines = lines + 1;
|
||||
let lineHeight = getGutterLineHeight(view);
|
||||
if (!lineHeight) {
|
||||
return null;
|
||||
}
|
||||
// for (var i = 0; i < gutters.length; i++) {
|
||||
// let node = gutters[i];
|
||||
// node.style.minHeight = `calc(${lineHeight} * ${_lines})`;
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleChange(vu: ViewUpdate): void {
|
||||
if (vu.docChanged) {
|
||||
const doc = vu.state.doc;
|
||||
const text = doc.toString();
|
||||
$nodeValue = text;
|
||||
dispatch("change", text);
|
||||
}
|
||||
view.requestMeasure({ read: updateGutters });
|
||||
}
|
||||
|
||||
function getBaseExtensions(readonly: boolean, placeholder: string | null): Extension[] {
|
||||
const extensions: Extension[] = [
|
||||
EditorView.editable.of(!readonly),
|
||||
EditorState.readOnly.of(readonly)
|
||||
];
|
||||
|
||||
extensions.push(basicSetup);
|
||||
|
||||
if (placeholder) {
|
||||
extensions.push(placeholderExt(placeholder));
|
||||
}
|
||||
|
||||
extensions.push(EditorView.updateListener.of(handleChange));
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
function getTheme(dark_mode: boolean): Extension[] {
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
if (dark_mode) {
|
||||
extensions.push(basicDark);
|
||||
} else {
|
||||
extensions.push(basicLight);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function getExtensions(): Extension[] {
|
||||
// TODO
|
||||
const readonly = false;
|
||||
const placeholder = "Placeholder..."
|
||||
const dark_mode = true;
|
||||
|
||||
const stateExtensions: Extension[] = [
|
||||
...getBaseExtensions(
|
||||
readonly,
|
||||
placeholder,
|
||||
),
|
||||
BaseTheme,
|
||||
...getTheme(dark_mode),
|
||||
...extraExtensions
|
||||
];
|
||||
return stateExtensions;
|
||||
}
|
||||
|
||||
function createEditorState(value: string | null | undefined): EditorState {
|
||||
return EditorState.create({
|
||||
doc: value ?? undefined,
|
||||
extensions: getExtensions()
|
||||
});
|
||||
}
|
||||
|
||||
function createEditorView(): EditorView {
|
||||
return new EditorView({
|
||||
parent: element,
|
||||
state: createEditorState($nodeValue)
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="codemirror-wrapper {classNames}" bind:this={element} />
|
||||
</div>
|
||||
<!-- <CodeMirror bind:value={$nodeValue} {styles} /> -->
|
||||
|
||||
<style lang="scss">
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-flow: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
border: 1px solid var(--input-border-color);
|
||||
|
||||
.codemirror-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.cm-scroller) {
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
55
src/lib/widgets/TextWidgetCodeVariant.ts
Normal file
55
src/lib/widgets/TextWidgetCodeVariant.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import {
|
||||
lineNumbers,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
keymap
|
||||
} from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import {
|
||||
foldGutter,
|
||||
indentOnInput,
|
||||
syntaxHighlighting,
|
||||
defaultHighlightStyle,
|
||||
foldKeymap
|
||||
} from "@codemirror/language";
|
||||
import { history, defaultKeymap, historyKeymap } from "@codemirror/commands";
|
||||
import {
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap
|
||||
} from "@codemirror/autocomplete";
|
||||
import { lintKeymap } from "@codemirror/lint";
|
||||
import {
|
||||
type CompletionSource, autocompletion, CompletionContext, startCompletion,
|
||||
currentCompletions, completionStatus, completeFromList, acceptCompletion
|
||||
} from "@codemirror/autocomplete"
|
||||
import DanbooruTags from "$lib/DanbooruTags";
|
||||
|
||||
export const basicSetup: Extension = /*@__PURE__*/ (() => [
|
||||
lineNumbers(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
closeBrackets(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
EditorView.lineWrapping,
|
||||
DanbooruTags.getCompletionExt(),
|
||||
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
])
|
||||
])();
|
||||
@@ -2,10 +2,10 @@
|
||||
import "$lib/nodeImports";
|
||||
|
||||
import ComfyApp from '$lib/components/ComfyApp';
|
||||
import { configureLitegraph } from '$lib/init';
|
||||
import init from '$lib/init';
|
||||
import App from './App.svelte';
|
||||
|
||||
configureLitegraph()
|
||||
init();
|
||||
|
||||
const comfyApp = new ComfyApp();
|
||||
(window as any).app = comfyApp;
|
||||
|
||||
@@ -9,11 +9,11 @@ import ComfyApp from '$lib/components/ComfyApp';
|
||||
import uiState from '$lib/stores/uiState';
|
||||
import { LiteGraph } from '@litegraph-ts/core';
|
||||
import ComfyGraph from '$lib/ComfyGraph';
|
||||
import { configureLitegraph } from '$lib/init';
|
||||
import init from '$lib/init';
|
||||
|
||||
Framework7.use(Framework7Svelte);
|
||||
|
||||
configureLitegraph(true);
|
||||
init(true);
|
||||
|
||||
const comfyApp = new ComfyApp();
|
||||
(window as any).app = comfyApp;
|
||||
|
||||
Reference in New Issue
Block a user