pull/178/head
Nevysha 2023-06-12 07:22:22 +02:00 committed by GitHub
parent ec0a01901b
commit b01770f28b
69 changed files with 5595 additions and 10209 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
Cozy-Nest.iml
nevyui_settings.json
data/images.cache

View File

@ -1,9 +1,19 @@
## Compatibility
- From Automatic1111's webui `5ab7f213` commit to the 1.2.1 release.
- Automatic1111's webui 1.3.1 release.
- SD.Next (Vlad's fork) from commit `beff89ba`
- Will work best on latest version of both as I'm only testing on latest version.
## New features in 2.2.0
- [x] Redo the Extra Network tweaks from scratch. User experiencing issues with previous version should not experience issues anymore.
- [x] Enhanced prompt editor with color (in txt2img and img2img) - It can be disabled through settings
- [x] Tag system for image browser : you can now add tag to your images and filter them by tag. Tags are saved in exif metadata.
- [x] Exif metadata editor : you can now edit exif metadata of your images
- [x] You can move img into a separated archive folder (set through settings)
- [x] You can hide image from image browser (a tag is added to the image exif)
- [x] You can delete images from image browser
- [x] Image browser now build a cache of its index to speed up loading time
## Minor changes & fixes in 2.1.7
- [x] Quick fix to be able to save settings despite the new ui-config.json save ui system. (pruning the file from Cozy Nest entry on startup)

View File

@ -17,10 +17,15 @@ Cozy Nest is a UI extension for Automatic's sd-webui. Inspired by [anapnoe](http
## Features:
- [x] Fully integrated Image Browser **IN BETA**. Lots of bugs and missing features. Please be kind with Github issues.
- [x] Send to txt2img / img2img / …
- [x] Search
- [x] Tag your images and filter by tag
- [x] Edit exif metadata
- [x] Archive, hide or delete images
- [x] Clean memory for image not visible (unload them / replace with dummy div) clean filteredImages and loadedImage Array
- [x] manage new image generated
- [x] Automatically get image output folder (without grid folder)
- [x] Drag and drop image
- [x] Enhanced prompt editor with color (in txt2img and img2img) - It can be disabled through settings
- [x] Resizable panels
- [x] Full Screen Inpainting
- [x] Customizable tab menu position (top, left, centered)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
},
}

View File

@ -29,6 +29,6 @@ npm run dev
```bash
cd Cozy-Nest/cozy-nest-client
# check output folder in vite.config.js. It is hard coded atm.
# check output folder in vite.config.ts. It is hard coded atm.
npm run build
```

View File

@ -0,0 +1,69 @@
import React, {useRef} from 'react'
import {useDisclosure} from "@chakra-ui/react";
import {
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
AlertDialogCloseButton,
Button,
} from '@chakra-ui/react'
export function ButtonWithConfirmDialog({message, confirmLabel, cancelLabel, onConfirm, style}) {
const { isOpen, onOpen, onClose } = useDisclosure()
const cancelRef = useRef()
if (style) {
style = {...style, width: '100%'}
}
else
style = {width: '100%'}
return (
<>
<button
className="btn-settings"
style={style}
onClick={onOpen}
>{confirmLabel}</button>
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
variant="nevysha-confirm"
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Confirm Action
</AlertDialogHeader>
<AlertDialogBody>
{message}
</AlertDialogBody>
<AlertDialogFooter>
<button
className="btn-settings"
ref={cancelRef}
onClick={onClose}>
{cancelLabel}
</button>
<button
className="btn-settings"
onClick={() => {
onClose();
onConfirm();
}}>
{confirmLabel}
</button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
)
}

View File

@ -0,0 +1,42 @@
import {createMultiStyleConfigHelpers} from "@chakra-ui/react";
import {checkboxAnatomy, radioAnatomy} from "@chakra-ui/anatomy";
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(checkboxAnatomy.keys)
export const checkboxTheme = defineMultiStyleConfig({
defaultProps: {
variant: 'nevysha'
},
variants: { nevysha: definePartsStyle({
control: {
boxShadow: 'var(--input-shadow)',
border: '1px solid var(--ae-input-border-color) !important',
borderRadius: 'var(--checkbox-border-radius)',
backgroundColor: 'var(--checkbox-background-color)',
lineHeight: 'var(--line-sm)',
}
}) },
})
const {
definePartsStyle: radioDefinePartsStyle,
defineMultiStyleConfig: radioDefineMultiStyleConfig
} = createMultiStyleConfigHelpers(radioAnatomy.keys);
export const radioTheme = radioDefineMultiStyleConfig({
defaultProps: {
variant: 'nevysha'
},
variants: { nevysha: radioDefinePartsStyle({
control: {
boxShadow: 'var(--input-shadow)',
border: '1px solid var(--ae-input-border-color) !important',
// borderRadius: 'var(--checkbox-border-radius)',
backgroundColor: 'var(--checkbox-background-color)',
lineHeight: 'var(--line-sm)',
}
}) },
})

View File

@ -0,0 +1,62 @@
import {createMultiStyleConfigHelpers} from "@chakra-ui/react";
import {inputAnatomy, numberInputAnatomy} from "@chakra-ui/anatomy";
let { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(inputAnatomy.keys)
export const inputTheme = defineMultiStyleConfig({
defaultProps: {
variant: 'nevysha'
},
variants: { nevysha: definePartsStyle({
field: {
outline: 'none!important',
boxShadow: 'var(--input-shadow)',
border: '1px solid var(--ae-input-border-color) !important',
borderRadius: '0 !important',
backgroundColor: 'var(--input-background-fill) !important',
padding: 'var(--input-padding) !important',
width: '100%',
color: 'var(--body-text-color)',
fontSize: 'var(--input-text-size)',
lineHeight: 'var(--line-sm)',
fontFamily: 'monospace !important',
}
}) },
})
const {
definePartsStyle: numberInputDefinePartsStyle,
defineMultiStyleConfig: numberInputDefineMultiStyleConfig
} = createMultiStyleConfigHelpers(numberInputAnatomy.keys);
export const numberInputTheme = numberInputDefineMultiStyleConfig({
defaultProps: {
variant: 'nevysha'
},
variants: { nevysha: numberInputDefinePartsStyle({
field: {
outline: 'none!important',
boxShadow: 'var(--input-shadow)',
border: '1px solid var(--ae-input-border-color) !important',
borderRadius: '0 !important',
backgroundColor: 'var(--input-background-fill) !important',
padding: 'var(--input-padding) !important',
width: '100%',
color: 'var(--body-text-color)',
fontSize: 'var(--input-text-size) !important',
lineHeight: 'var(--line-sm)',
fontFamily: 'monospace !important',
},
stepperGroup: {
border: '1px solid var(--ae-input-border-color) !important',
borderTop: 'none !important',
borderRight: 'none !important',
},
stepper: {
color: 'var(--body-text-color)',
borderTop: 'none !important',
borderLeft: 'none !important',
}
}) },
})

View File

@ -0,0 +1,45 @@
import {createMultiStyleConfigHelpers} from "@chakra-ui/react";
import {modalAnatomy} from "@chakra-ui/anatomy";
let { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(modalAnatomy.keys)
export const modalTheme = defineMultiStyleConfig({
defaultProps: {
variant: 'nevysha'
},
variants: {
nevysha:
definePartsStyle({
dialog: {
opacity: 1,
width: '800px',
marginRight: 'auto',
marginLeft: 'auto',
transform: 'none',
maxWidth: 'fit-content',
background: 'none',
},
}),
'nevysha-confirm':
definePartsStyle({
dialog: {
opacity: 1,
width: '800px',
marginRight: 'auto',
marginLeft: 'auto',
transform: 'none',
maxWidth: 'fit-content',
border: '1px solid var(--ae-input-border-color)',
backgroundColor: 'var(--block-background-fill)',
color: 'var(--body-text-color)',
borderRadius: '0 !important',
fontSize: 'var(--body-text-size)',
},
footer: {
display: 'flex !important',
gap: '5px',
}
})
},
})

View File

@ -0,0 +1,101 @@
import {createMultiStyleConfigHelpers} from "@chakra-ui/react";
import {tabsAnatomy} from "@chakra-ui/anatomy";
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(tabsAnatomy.keys)
export const tabsTheme = defineMultiStyleConfig({
defaultProps: {
variant: 'nevysha'
},
variants: {
nevysha: definePartsStyle({
tab: {
marginBottom: '-1px',
border: '1px solid transparent',
borderColor: 'transparent',
borderBottom: 'none',
borderRadius: '0 !important',
padding: 'var(--size-1) var(--size-4) !important',
color: 'var(--body-text-color-subdued) !important',
fontWeight: 'var(--section-header-text-weight) !important',
fontSize: 'var(--section-header-text-size) !important',
borderTop: '2px solid transparent !important',
_selected: {
borderTop: '2px solid var(--ae-primary-color) !important',
backgroundColor: 'var(--tab-nav-background-color-selected) !important',
color: 'var(--body-text-color) !important',
},
_focus: {
outline: 'none'
},
_hover: {
outline: 'none',
borderRight: '1px solid transparent',
borderLeft: '1px solid transparent',
}
},
tabpanel: {
border: '1px solid var(--border-color-primary)',
borderTop: 'none',
borderBottomRightRadius: 'var(--container-radius)',
borderBottomLeftRadius: 'var(--container-radius)',
padding: 'var(--block-padding)',
backgroundColor: 'var(--tab-nav-background-color-selected) !important',
gap: '20px',
display: 'flex',
flexDirection: 'column',
height: '500px',
overflowY: 'auto',
}
}),
nevyshaExtraNetwork: definePartsStyle({
root: {
height: '100% !important',
display: 'flex',
flexDirection: 'column'
},
tabpanels: {
height: '100% !important',
},
tab: {
marginBottom: '-1px',
border: '1px solid transparent',
borderColor: 'transparent',
borderBottom: 'none',
borderRadius: '0 !important',
padding: 'var(--size-1) var(--size-4) !important',
color: 'var(--body-text-color-subdued) !important',
fontWeight: 'var(--section-header-text-weight) !important',
fontSize: 'var(--section-header-text-size) !important',
borderTop: '2px solid transparent !important',
_selected: {
borderTop: '2px solid var(--ae-primary-color) !important',
backgroundColor: 'var(--tab-nav-background-color-selected) !important',
color: 'var(--body-text-color) !important',
},
_focus: {
outline: 'none'
},
_hover: {
outline: 'none',
borderRight: '1px solid transparent',
borderLeft: '1px solid transparent',
}
},
tabpanel: {
border: '1px solid var(--border-color-primary)',
borderTop: 'none',
borderBottomRightRadius: 'var(--container-radius)',
borderBottomLeftRadius: 'var(--container-radius)',
padding: 'var(--block-padding)',
backgroundColor: 'var(--tab-nav-background-color-selected) !important',
gap: '20px',
display: 'flex',
flexDirection: 'column',
height: '100%',
overflowY: 'auto',
}
})
},
})

View File

@ -0,0 +1,21 @@
import { extendTheme } from '@chakra-ui/react'
import {inputTheme, numberInputTheme} from "./Input.theme";
import {tabsTheme} from "./Tabs.theme";
import {checkboxTheme, radioTheme} from "./Checkbox.theme";
import {modalTheme} from "./Modal.theme";
export const theme = extendTheme({
fontSizes: {
md: 'var(--body-text-size)',
},
components: {
Input: inputTheme,
Tabs: tabsTheme,
Checkbox: checkboxTheme,
NumberInput: numberInputTheme,
Radio: radioTheme,
Modal: modalTheme,
},
})

View File

@ -0,0 +1,160 @@
import React, {useEffect, useRef, useState} from "react";
import 'ace-builds'
// ace.config.setModuleUrl(
// "ace/mode/json_worker",
// 'cozy-nest-client/node_modules/ace-builds/src-noconflict/worker-json.js')
ace.config.setModuleUrl(
"ace/mode/prompt_highlight_rules",
"cozy-nest-client/cozy-prompt/prompt_highlight_rules.js");
ace.config.setModuleUrl(
"ace/mode/prompt",
"cozy-nest-client/cozy-prompt/mode-prompt.js");
import AceEditor from "react-ace";
import "./prompt_highlight_rules.js";
import "./mode-prompt.js";
import "ace-builds/src-noconflict/theme-github_dark";
import "ace-builds/src-noconflict/ext-language_tools";
import './CozyPrompt.css'
import useExternalTextareaObserver from "./useExternalTextareaObserver.js";
import {Button} from "../image-browser/App.jsx";
import {Row} from "../main/Utils.jsx";
export function App({parentId, containerId}) {
let savedHeight = localStorage.getItem(`cozy-prompt-height-${containerId}`);
savedHeight = savedHeight ? parseInt(savedHeight) : 200;
const nativeTextarea = document.querySelector(`#${parentId} label textarea`);
const [nativeIsVisible, setNativeIsVisible] = useState(false);
const nativeTextareaValue = useExternalTextareaObserver(`#${parentId} label textarea`);
const [prompt, setPrompt] = useState('');
const editor = useRef();
const [height, setHeight] = useState(savedHeight);
const [dragging, setDragging] = useState(false);
const [startY, setStartY] = useState(0);
const propagate = () => {
nativeTextarea.value
= prompt
const event = new Event('input')
nativeTextarea.dispatchEvent(event)
}
useEffect(() => {
const handlePromptChange = (event) => {
setPrompt(event.target.value)
}
nativeTextarea.addEventListener('change', handlePromptChange)
return () => {
nativeTextarea.removeEventListener('change', handlePromptChange)
}
}, []);
useEffect(() => {
setPrompt(nativeTextareaValue)
}, [nativeTextareaValue]);
useEffect(() => {
if (!nativeIsVisible) {
nativeTextarea.style.display = 'none';
}
else {
nativeTextarea.style.display = 'block';
//margin-top: 40px;
nativeTextarea.style.marginTop = '40px';
}
}, [nativeIsVisible]);
useEffect(() => {
const handleGlobalMouseUp = () => {
if (dragging) {
setDragging(false);
}
};
const handleGlobalMouseMove = (event) => {
if (dragging) {
const newHeight = height + event.clientY - startY;
setHeight(newHeight);
localStorage.setItem(`cozy-prompt-height-${containerId}`, String(newHeight));
setStartY(event.clientY);
}
};
window.addEventListener('mouseup', handleGlobalMouseUp);
window.addEventListener('mousemove', handleGlobalMouseMove);
return () => {
window.removeEventListener('mouseup', handleGlobalMouseUp);
window.removeEventListener('mousemove', handleGlobalMouseMove);
};
}, [dragging]);
const handleMouseDown = (event) => {
event.preventDefault();
setDragging(true);
setStartY(event.clientY);
};
const toggleNative = () => {
setNativeIsVisible(!nativeIsVisible);
}
function prettify() {
setPrompt(prompt.replaceAll('),', '),\n'))
}
function onLoadEditor(editor) {
editor.renderer.setPadding(10);
editor.renderer.setScrollMargin(10);
}
return (
<div
className="CozyPrompt"
style={{ height: `${height}px` }}
>
<AceEditor
ref={editor}
onLoad={onLoadEditor}
mode="prompt"
theme="github_dark"
showPrintMargin={false}
onChange={setPrompt}
onBlur={propagate}
value={prompt}
name="ace-prompt-editor"
editorProps={{ $blockScrolling: true }}
style={{width: "100%", height: "100%"}}
setOptions={{
animatedScroll: true,
enableSnippets: true,
cursorStyle: "smooth",
behavioursEnabled: true,
wrapBehavioursEnabled: true,
autoScrollEditorIntoView: true,
wrap: true,
fontSize: "15px",
fontFamily: "monospace",
}}
/>
<div
onMouseDown={handleMouseDown}
className="CozyPrompt__resize-handle"
/>
<Row>
<Button onClick={prettify}>Prettify</Button>
<Button onClick={toggleNative}>{nativeIsVisible ? "Hide" : "Show"} native textarea</Button>
</Row>
</div>
);
}

View File

@ -0,0 +1,98 @@
.CozyPrompt {
min-height: 100px;
max-height: 800px;
width: 100%;
border: 1px solid var(--ae-input-border-color) !important;
}
.CozyPrompt .container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.CozyPrompt .ace_cursor-layer {
z-index: 900;
}
.CozyPrompt .ace_cursor-layer .ace_cursor {
z-index: 901;
background-color: var(--ae-primary-color);
position: fixed;
}
@keyframes blink-ace-animate {
from, to { opacity: 0.5; }
60% { opacity: 0; }
}
@keyframes blink-ace-animate-smooth {
from, to { opacity: 0.5; }
45% { opacity: 0.5; }
60% { opacity: 0; }
85% { opacity: 0; }
}
.CozyPrompt .ace_active-line {
background: #ffffff0d !important;
}
.CozyPrompt {
/*padding: 10px;*/
background-color: var(--input-background-fill) !important;
}
.CozyPrompt .ace_editor {
background-color: var(--input-background-fill) !important;
}
.CozyPrompt .ace_scrollbar::-webkit-scrollbar {
width: 5px;
}
.CozyPrompt .ace_scrollbar::-webkit-scrollbar-track {
background-color: transparent;
}
.CozyPrompt .ace_scrollbar::-webkit-scrollbar-thumb {
background-color: var(--ae-primary-color);
border-radius: 20px;
}
.CozyPrompt__resize-handle {
width: 100%;
height: 5px;
background-color: var(--ae-input-border-color);
cursor: row-resize;
}
/*Editor color*/
.ace_text-layer {
color: var(--body-text-color);
font-family: monospace !important;
}
.ace_open-bracket, .ace_close-bracket {
color: var(--ae-primary-color);
font-weight: bold;;
font-size: 1.1em;
}
.ace_open-bracket-0, .ace_close-bracket-0 {
color: violet;
}
.ace_open-bracket-1, .ace_close-bracket-1 {
color: var(--ae-primary-color);
}
.ace_open-bracket-2, .ace_close-bracket-2 {
color: hotpink;
}
.ace_open-bracket-3, .ace_close-bracket-3 {
color: greenyellow;
}
.ace_open-bracket-4, .ace_close-bracket-4 {
color: #fa662a;
}
.ace_token {
color: #cccc96;
font-weight: bold;
/*margin: 0 3px;*/
}
.ace_lora-begin, .ace_lora-end, .ace_lora-inner {
color: #c444d5;
}
.ace_lora-begin, .ace_lora-end {
font-weight: bold;
font-size: 1.1em;
filter: brightness(1.2);
}

View File

@ -0,0 +1,24 @@
import React from "react";
import ReactDOM from 'react-dom/client'
import {App} from "./App.jsx";
export default function startCozyPrompt(parentId, containerId) {
//
if (!document.getElementById(parentId)) {
setTimeout(() => startCozyPrompt(), 200)
return
}
const settingsDiv = document.createElement("div");
settingsDiv.id = containerId;
settingsDiv.style = 'display: flex; height: fit-content; width: 100%;'
document.getElementById(parentId)
.insertBefore(settingsDiv, document.getElementById(parentId).firstChild);
ReactDOM.createRoot(document.getElementById(containerId)).render(
<React.StrictMode>
<App containerId={containerId} parentId={parentId}/>
</React.StrictMode>,
)
}

View File

@ -0,0 +1,25 @@
import {CozyLogger} from "../main/CozyLogger.js";
ace.define("ace/mode/prompt", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text"], function (require, exports, module) {
const oop = require("ace/lib/oop");
const TextMode = require("ace/mode/text").Mode;
const CustomHighlightRules = require("ace/mode/prompt_highlight_rules").CustomHighlightRules;
// Define the CustomMode constructor
function CustomMode() {
this.HighlightRules = CustomHighlightRules;
}
// Inherit from the base TextMode
oop.inherits(CustomMode, TextMode);
// Set the mode's name
CustomMode.prototype.$id = "ace/mode/prompt";
(function() {
CozyLogger.debug(`what is this:`,this, arguments);
}).call(CustomMode.prototype);
// Export the mode
exports.Mode = CustomMode;
});

View File

@ -0,0 +1,71 @@
ace.define("ace/mode/prompt_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function (require, exports, module) {
const oop = require("ace/lib/oop");
const TextHighlightRules = require("ace/mode/text_highlight_rules").TextHighlightRules;
// Define the CustomHighlightRules constructor
function CustomHighlightRules() {
// Create an instance of the base TextHighlightRules
TextHighlightRules.call(this);
// Define the regex patterns for different token types
const openBracket = /[\(\[\{]/;
const closeBracket = /[\)\]\}]/;
let bracketLevel = 0;
this.$rules = {
start: [
{
token: () => {
bracketLevel++;
return `open-bracket.open-bracket-${(bracketLevel) % 4}`;
},
next: "inner",
regex: openBracket
},
{
token: () => {
bracketLevel--;
return `close-bracket.close-bracket-${(bracketLevel+1) % 4}`;
},
regex: closeBracket,
next: "start"
},
{ regex: /<lora:/, token: "lora-begin", next: "lora" },
{ regex: /[,|:]/, token: "token" },
{ regex: /\w+/, token: "text" },
],
lora: [
{ regex: '>', token: "lora-end", next: "start" },
{ regex: /\w+/, token: "lora-inner" },
],
inner: [
{
token: () => {
bracketLevel++;
return `open-bracket.open-bracket-${(bracketLevel) % 4}`;
},
regex: openBracket,
next: "inner"
},
{ regex: /[,|:]/, token: "token" },
{ regex: /\w+/, token: "inner-bracket" },
{
token: () => {
bracketLevel--;
return `close-bracket.close-bracket-${(bracketLevel+1) % 4}`;
},
regex: closeBracket,
next: "start"
},
]
};
}
// Inherit from the base TextHighlightRules
oop.inherits(CustomHighlightRules, TextHighlightRules);
// Export the highlight rules
exports.CustomHighlightRules = CustomHighlightRules;
});

View File

@ -0,0 +1,40 @@
import { useState, useEffect } from 'react';
import {CozyLogger} from "../main/CozyLogger.js";
const useExternalTextareaObserver = (textareaSelector) => {
const [value, setValue] = useState('');
useEffect(() => {
const observerCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes') {
const externalTextarea = document.querySelector(textareaSelector);
setValue(externalTextarea.value);
}
}
};
const observerOptions = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
};
const observer = new MutationObserver(observerCallback);
const externalTextarea = document.querySelector(textareaSelector);
if (externalTextarea) {
observer.observe(externalTextarea, observerOptions);
setValue(externalTextarea.value);
}
return () => {
observer.disconnect();
};
}, [textareaSelector]);
return value;
};
export default useExternalTextareaObserver;

13
cozy-nest-client/cozy-types.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// type for image
export type Image = {
path: string,
hash: string,
metadata: {
date: number
exif: {
parameters: string,
'cozy-nest-tags'?: string
'cozy-nest-hidden'?: string
}
}
}

View File

@ -0,0 +1,66 @@
.main-btn-label {
writing-mode: vertical-rl;
width: 25px;
min-width: 25px !important;
}
.cozy-extra-network {
display: flex;
flex-direction: column;
height: calc(100% - 20px);
}
.cozy-extra-network > div > .tabs > .tabitem {
height: 100%;
overflow-y: auto;
}
.cozy-extra-network > div > .tabs {
height: 100%;
}
#extra_networks_wrapper {
position: fixed;
top: calc(75px + var(--menu-top-height));
height: calc(100% - (100px + var(--menu-top-height)));
right: 0;
z-index: 9999;
padding-right: 15px;
display: flex;
flex-direction: row;
background-color: var(--block-background-fill) !important;
}
.ExtraNetworkTab {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.ExtraNetworkTab::-webkit-scrollbar {
width: 5px;
}
.ExtraNetworkTab::-webkit-scrollbar-track {
background-color: transparent;
}
.ExtraNetworkTab::-webkit-scrollbar-thumb {
background-color: var(--ae-primary-color);
border-radius: 20px;
}
.cozy-extra-network > div > .tabs > .tabitem::-webkit-scrollbar {
width: 5px;
}
.cozy-extra-network > div > .tabs > .tabitem::-webkit-scrollbar-track {
background-color: transparent;
}
.cozy-extra-network > div > .tabs > .tabitem::-webkit-scrollbar-thumb {
background-color: var(--ae-primary-color);
border-radius: 20px;
}

View File

@ -0,0 +1,46 @@
import React, {useEffect} from "react";
import './ExtraNetworks.css'
import {LoaderContext} from "./LoaderContext.jsx";
import {CozyLogger} from "../main/CozyLogger.js";
let extraNetworksParent = null;
export function ExtraNetworks({prefix}) {
const ref = React.useRef(null)
const {ready} = React.useContext(LoaderContext)
useEffect(() => {
if (ready) {
loadNativeElements()
}
return () => {
unLoad()
}
}, [ready])
function loadNativeElements() {
if (!ref.current) return
CozyLogger.debug('loading native elements', prefix)
const tabs = document.querySelector(`#${prefix}_extra_tabs`)
extraNetworksParent = tabs.parentNode
ref.current.appendChild(tabs)
}
function unLoad() {
if (!ref.current || !extraNetworksParent) return
CozyLogger.debug('unloading native elements', prefix)
const tabs = document.querySelector(`#${prefix}_extra_tabs`)
extraNetworksParent.appendChild(tabs)
}
return (
<div ref={ref} style={{height:'100%'}} />
);
}

View File

@ -0,0 +1,91 @@
import React from 'react';
import {CozyLogger} from "../main/CozyLogger.js";
export const LoaderContext = React.createContext({
ready: false,
});
function observeDivChanges(targetDiv) {
return new Promise((resolve) => {
let timer; // Holds the timeout reference
const observer = new MutationObserver((mutationsList, observer) => {
clearTimeout(timer); // Clear previous timeout
timer = setTimeout(() => {
observer.disconnect(); // Stop observing mutations
resolve(); // Resolve the Promise
}, 200);
});
observer.observe(targetDiv, { attributes: true, childList: true, subtree: true });
// If the initial state of the div is already unchanged, resolve the Promise immediately
if (!targetDiv.hasChildNodes() && !targetDiv.attributes.length) {
resolve();
}
});
}
async function requireNativeBloc(prefix) {
const triggerButton = document.querySelector(`button#${prefix}_extra_networks`)
CozyLogger.debug('triggering extra network', prefix)
triggerButton.style.display = 'none'
triggerButton.click()
const tabs = document.querySelector(`#${prefix}_extra_tabs`)
//setup a mutation observer to detect when the tabs are added
await observeDivChanges(tabs)
triggerButton.click()
CozyLogger.debug('tabs loaded', prefix)
}
//we use a local not hook to avoid async issues and double call
const states = {}
export function LoaderProvider({children, prefix, resolve}) {
const [ready, setReady] = React.useState(false)
if (!states[prefix]) {
states[prefix] = {
loaded: false,
loading: false,
}
}
React.useEffect(() => {
const {ready, loading} = states[prefix];
if (ready || loading) return;
states[prefix] = {
loaded: false,
loading: true,
};
(async () => {
await requireNativeBloc(prefix)
states[prefix] = {
loaded: true,
loading: false,
}
setReady(true)
resolve()
})()
}, [])
const value = {
ready,
}
return (
<LoaderContext.Provider value={value}>
{children}
</LoaderContext.Provider>
)
}

View File

@ -0,0 +1,31 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import {ExtraNetworks} from "./ExtraNetworks.jsx";
import {LoaderProvider} from "./LoaderContext.jsx";
import {CozyLogger} from "../main/CozyLogger.js";
export function startExtraNetwork(prefix) {
return new Promise((resolve, reject) => {
_startExtraNetwork(prefix, resolve)
})
}
function _startExtraNetwork(prefix, resolve) {
CozyLogger.debug('startExtraNetwork', prefix)
if (!document.getElementById(`cozy-${prefix}-extra-network-react`)) {
CozyLogger.debug('waiting for extra network react', prefix)
setTimeout(() => _startExtraNetwork(), 200)
return
}
ReactDOM.createRoot(document.getElementById(`cozy-${prefix}-extra-network-react`)).render(
<React.StrictMode>
<LoaderProvider prefix={prefix} resolve={resolve}>
<ExtraNetworks prefix={prefix} />
</LoaderProvider>
</React.StrictMode>,
)
}

View File

@ -9,20 +9,13 @@
font-weight: bold;
text-transform: uppercase;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-row {
display: flex;
flex-direction: row;
}
.browser {
height: 100%;
/*overflow-y: scroll;*/
display: flex;
flex-wrap: wrap;
overflow: auto !important;
align-content: flex-start;
}
.hackyOffPageElement {
@ -62,6 +55,7 @@
border: 1px solid var(--ae-input-border-color);
background: var(--ae-input-bg-color);
color: var(--ae-input-color);
gap: 5px;
}
.infoModal {
@ -123,74 +117,63 @@ textarea {
/* margin-bottom: 6px; */
position: absolute;
color: #e32f4e;
/* left: 5px; */
top: 5px;
}:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
left: 2px;
top: 2px;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
.cnib-button {
width: 100%;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
input[type="radio"] {
display: inline-block;
flex-shrink: 0;
vertical-align: middle;
appearance: none;
border-width: 1px;
border-color: #6b7280;
background-origin: border-box;
padding: 0;
width: 1rem;
height: 1rem;
color: #2563eb;
user-select: none;
--ring-color: transparent;
position: relative;
box-shadow: var(--checkbox-shadow);
border: var(--checkbox-border-width) solid var(--checkbox-border-color);
border-radius: var(--radius-full);
background-color: var(--checkbox-background-color) !important;
line-height: var(--line-sm);
margin-right: 4px;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
.cozy-radio-label {
text-overflow: clip;
white-space: nowrap;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
.cozy-websocket-status {
justify-content: end;
display: flex;
flex-direction: column;
width: fit-content;
align-items: flex-end;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
.cozy-websocket-status > span {
text-overflow: clip;
white-space: nowrap;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
.cozy-nest-loading {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-content: center;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 25px;
}

View File

@ -0,0 +1,287 @@
import React, {useEffect, useState, useCallback, useContext} from 'react'
import './App.css'
import useWebSocket, { ReadyState } from 'react-use-websocket';
import Browser from "./Browser.jsx";
import {MockImageBrowser} from "./MockImageBrowser.jsx";
import {CozyLogger} from "../main/CozyLogger.js";
import Loader from "react-spinners/HashLoader";
import {ImagesContext} from "./ImagesContext.tsx";
import {Column, Row} from "../main/Utils.jsx";
import {CozyTagsSelect} from "./CozyTags.jsx";
export function Button(props) {
return <button
{...props}
className="nevysha lg primary gradio-button btn cnib-button"
>{props.children}</button>
}
function Loading(props) {
const config = JSON.parse(localStorage.getItem('COZY_NEST_CONFIG'))
const color = config['accent_color'] || '#36d7b7'
const label = props.label || ''
return (
<div className='cozy-nest-loading'>
<div>{label}</div>
<Loader color={color} />
</div>
)
}
function App() {
const config = JSON.parse(localStorage.getItem('COZY_NEST_CONFIG'))
const disable_image_browser =
config['disable_image_browser']
const serverPort = (() => {
try {
return config.server_default_port
}
catch (e) {
CozyLogger.debug('cnib_socket_server_port not found in main gradio app')
return 3333;
}
})();
if (disable_image_browser) {
return (
<>
<MockImageBrowser/>
</>
)
}
const {
images,
setImages,
setFilteredImages,
setTags,
} = useContext(ImagesContext)
const [socketUrl, setSocketUrl] = useState(`ws://localhost:${serverPort}`);
const [, setMessageHistory] = useState([]);
const [activeTags, setActiveTags] = useState([])
const [searchStr, setSearchStr] = useState('');
const [emptyFetch, setEmptyFetch] = useState(false);
const [visibilityFilter, setVisibilityFilter] = useState('radio-hide-hidden');
const [isLoading, setIsLoading] = useState(true);
const { sendMessage, lastMessage, readyState, getWebSocket }
= useWebSocket(
socketUrl,
{
shouldReconnect: () => disable_image_browser,
reconnectAttempts: 10,
reconnectInterval: 3000,
}
);
//cheat to get the websocket object
window.ws = getWebSocket();
const askForImages = useCallback(() => sendMessage(
JSON.stringify({what: "images"})), [sendMessage]
);
const reconnect = () => {
if (readyState === ReadyState.OPEN) {
return;
}
// force a dummy url change
setSocketUrl(socketUrl + '?t=' + Date.now())
}
function applyActiveFilter() {
return images
.filter(image => {
if (visibilityFilter === 'radio-hide-hidden') {
return image.metadata.exif['cozy-nest-hidden'] !== 'True';
} else if (visibilityFilter === 'radio-only-hidden') {
return !(!image.metadata.exif['cozy-nest-hidden'] || image.metadata.exif['cozy-nest-hidden'] !== 'True');
} else return true;
})
.filter(image => {
if (activeTags.length === 0) {
return true;
}
else {
if (image.metadata.exif['cozy-nest-tags']) {
const imgTags = image.metadata.exif['cozy-nest-tags'].split(',')
const intersection = imgTags.filter(tag => activeTags.includes(tag))
return intersection.length > 0;
}
else return false;
}
})
}
//get images from server and set state
useEffect(() => {
if (lastMessage !== null) {
const data = JSON.parse(lastMessage.data)
if (data.error) {
if (window.errorPipe) {
window.errorPipe(data)
}
}
if (data.what === 'images') {
if (data.images.length === 0) {
CozyLogger.debug('Received empty images array from socket')
//disable images fetch loop
setEmptyFetch(true)
}
setImages(data.images)
setIsLoading(false)
}
if (data.what === 'dispatch_on_image_saved') {
//add at the beginning of the array
setImages(prev => [data.data, ...prev])
}
if (data.what === 'dispatch_on_index_built') {
setImages([...data.data])
setIsLoading(false)
}
setMessageHistory((prev) => prev.concat(lastMessage));
}
}, [lastMessage, setMessageHistory]);
//if images is empty, load images
useEffect(() => {
if (images.length === 0 && readyState === ReadyState.OPEN && !emptyFetch) {
askForImages()
}
else {
setFilteredImages(applyActiveFilter())
}
}, [images, readyState])
//if searchStr is not empty, filter images
useEffect(() => {
if (searchStr !== '') {
const _filteredImages = applyActiveFilter().filter(image => {
return JSON.stringify(image.metadata.exif).includes(searchStr);
})
setFilteredImages(_filteredImages)
}
else {
setFilteredImages(applyActiveFilter())
}
}, [searchStr, visibilityFilter, activeTags, images])
useEffect(() => {
const _tags = []
images.forEach(image => {
if (image.metadata.exif['cozy-nest-tags']) {
const imgTags = image.metadata.exif['cozy-nest-tags'].split(',')
_tags.push(...imgTags)
}
})
setTags([..._tags])
}, [images, visibilityFilter])
const connectionStatus = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Connected',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState];
const connexionStatusStyle = {
color:
readyState === ReadyState.OPEN
? 'green'
: readyState === ReadyState.CONNECTING
? 'orange'
: 'red',
};
const rebuildIndex = async () => {
//fetch to @app.delete("/cozy-nest/index")
const res = await fetch('/cozy-nest/index', {
method: 'DELETE',
})
if (res.ok) {
setImages([])
setIsLoading(true)
}
}
return (
<>
<Column>
<Row>
<Row>
<h1 className="cnib-title"><span className="beta-emphasis">beta</span></h1>
<button
className="nevysha lg primary gradio-button btn"
style={{width: '100px'}}
onClick={rebuildIndex}
>
Rebuild Index
</button>
</Row>
<Row style={{width: 'auto'}} className='cozy-websocket-status'>
<span>WebSocket status <span className="connexionStatus" style={connexionStatusStyle}>{connectionStatus}</span></span>
{readyState !== ReadyState.OPEN && <button
className="nevysha lg primary gradio-button btn"
style={{marginLeft: '20px', width: '100px'}}
onClick={reconnect}
>
Connect
</button>}
</Row>
</Row>
<Row>
<Row style={{gap:'10px', marginRight: '2px', width:'fit-content'}} onChange={(e) => setVisibilityFilter(e.target.id)}>
{/*radio button for filter : Hide hidden, All, Only hidden*/}
<Row style={{width: 'auto', alignItems: 'center'}}>
<input type="radio" id="radio-hide-hidden" name="radio-filter" value="all" defaultChecked/>
<label className="cozy-radio-label" htmlFor="radio-hide-hidden">Hide hidden</label>
</Row>
<Row style={{width: 'auto', alignItems: 'center'}}>
<input type="radio" id="radio-all" name="radio-filter" value="all"/>
<label className="cozy-radio-label" htmlFor="radio-all">All</label>
</Row>
<Row style={{width: 'auto', alignItems: 'center'}}>
<input type="radio" id="radio-only-hidden" name="radio-filter" value="hidden"/>
<label className="cozy-radio-label" htmlFor="radio-only-hidden">Only hidden</label>
</Row>
</Row>
</Row>
<Row>
<textarea data-testid="textbox"
placeholder="Search anything : Tags, Prompt, Size, Model, ..."
rows="1"
spellCheck="false"
data-gramm="false"
onChange={(e) => setSearchStr(e.target.value)}/>
<CozyTagsSelect setActiveTags={setActiveTags} />
</Row>
</Column>
{!isLoading &&
<Browser />
}
{isLoading && <Loading label="building Index..."/>}
</>
)
}
export default App

View File

@ -0,0 +1,68 @@
import React, {useContext, useEffect, useRef, useState} from "react";
import CozyImage from "./CozyImage.jsx";
import {ImagesContext} from "./ImagesContext.tsx";
const _LAZY_LOAD_MARGIN = 300
export default function Browser(props) {
const {images, filteredImages} = useContext(ImagesContext)
const _me = useRef(null)
const [page, setPage] = useState(0)
const [imagesLoaded, setImagesLoaded] = useState([])
const [viewPort, setViewPort] = useState({
top: 0,
bottom: window.innerHeight + _LAZY_LOAD_MARGIN
})
//when imagesRef changes, reset imagesLoaded
useEffect(() => {
setImagesLoaded([...filteredImages.slice(0, Math.min(page*20+20, filteredImages.length))])
}, [filteredImages, images])
//load 20 images on mount when imagesRef is set
if (filteredImages.length > 0 && imagesLoaded.length === 0) {
setImagesLoaded(filteredImages.slice(0, Math.min(20, filteredImages.length)))
}
const scrollHandler = () => {
maybeLoadMore()
let _page = Math.floor(imagesLoaded.length / 20)
if (_page !== page) {
setPage(_page)
}
const _viewPort = {
top: (_me.current.scrollTop - _LAZY_LOAD_MARGIN) > 0 ? (_me.current.scrollTop - _LAZY_LOAD_MARGIN) : 0,
bottom: _me.current.scrollTop + _me.current.clientHeight + _LAZY_LOAD_MARGIN
}
setViewPort(_viewPort)
}
const maybeLoadMore = () => {
//check if loadMoreThreshold is visible
const loadMoreThreshold = document.getElementById("loadMoreThreshold")
if (loadMoreThreshold.getBoundingClientRect().top < window.innerHeight) {
//load 20 more images
setImagesLoaded(filteredImages.slice(0, page*20+20))
}
}
return <div className="browser nevysha nevysha-scrollable" onScroll={() => scrollHandler()} ref={_me}>
{imagesLoaded.map((image, index) => {
return (
<CozyImage
key={image.hash}
imageHash={image.hash}
index={index}
viewPort={viewPort}/>
)
})}
<div id="loadMoreThreshold" className="hackyOffPageElement"/>
</div>;
}

View File

@ -0,0 +1,124 @@
import React, {useContext, useEffect, useState} from "react";
import {CozyLogger} from "../main/CozyLogger.js";
import {Button} from "./App.jsx";
import {Column, Row} from "../main/Utils.jsx";
import './editor/ExifEditor.css'
import Exif from "./editor/ExifEditor.jsx";
import {ImagesContext} from "./ImagesContext.tsx";
import {ButtonWithConfirmDialog} from "../chakra/ButtonWithConfirmDialog.jsx";
function SendTo({imageHash}) {
const {images, getImage} = useContext(ImagesContext)
const [image, setImage] = useState(
getImage(imageHash)
);
useEffect(() => {
setImage(getImage(imageHash));
}, [images, imageHash]);
const sendToPipe = (e, where) => {
e.preventDefault()
e.stopPropagation()
if (window.sendToPipe) {
let _img = {src: `/cozy-nest/image?path=${image.path}`}
window.sendToPipe(where, _img)
}
}
return <Row>
<Button onClick={(e) => sendToPipe(e, 'txt2img')}
>txt2img</Button>
<Button onClick={(e) => sendToPipe(e, 'img2img')}
>img2img</Button>
<Button onClick={(e) => sendToPipe(e, 'inpainting')}
>inpainting</Button>
</Row>;
}
export function Controls({imageHash}) {
const {images, deleteImg, updateExifInState, getImage} = useContext(ImagesContext)
const [showExifEditor, setShowExifEditor] = useState(false);
const [exif, setExif] = useState({});
const [isHidden, setIsHidden] = useState(false);
const [image, setImage] = useState(
getImage(imageHash)
);
useEffect(() => {
setImage(getImage(imageHash));
}, [images, imageHash]);
useEffect(() => {
if (!image || !image.path) return
setExif(image.metadata.exif)
}, [image, imageHash])
useEffect(() => {
if (!exif) return;
setIsHidden(exif['cozy-nest-hidden'] === 'True')
}, [exif, image, images])
const editExif = async () => {
setShowExifEditor(true)
}
const hideImg = async () => {
const path = image.path
exif['cozy-nest-hidden'] = "True"
setExif(exif)
await Exif.save(path, exif)
updateExifInState(image)
}
const unhideImg = async () => {
const path = image.path
exif['cozy-nest-hidden'] = "False"
setExif(exif)
await Exif.save(path, exif)
updateExifInState(image)
}
const ExifEditor = Exif.ExifEditor
return (
<Column style={{height: "100%", justifyContent: "space-between"}}>
<SendTo imageHash={imageHash}/>
<Column>
<Row>
{showExifEditor &&
<ExifEditor imageHash={imageHash} exif={exif} visible={showExifEditor}
onClose={() => setShowExifEditor(false)}/>
}
<Button onClick={editExif}>Edit Exif</Button>
</Row>
<Row>
{!isHidden && <Button onClick={hideImg}>Hide</Button>}
{isHidden && <Button onClick={unhideImg}>Show</Button>}
<Button onClick={() => deleteImg('archive', image)}>Move to archive</Button>
<ButtonWithConfirmDialog
style={{height: '100%'}}
message='This action cannot be undone. Are you sure?'
confirmLabel='Delete'
cancelLabel="Cancel"
onConfirm={() => deleteImg('delete', image)}
/>
</Row>
</Column>
</Column>
);
}

View File

@ -0,0 +1,80 @@
//base url without port
import React, {useContext, useEffect, useRef, useState} from "react";
import {CozyImageInfo} from "./CozyImageInfo.jsx";
import {ImagesContext} from "./ImagesContext.tsx";
const baseUrl = window.location.href.split(":")[0] + ":" + window.location.href.split(":")[1]
const gradioPort = 7860
export default function CozyImage({viewPort, imageHash, index}) {
const [showModal, setShowModal] = useState(false);
const imgRef = useRef(null);
const _me = useRef(null);
const {getImage} = useContext(ImagesContext)
const [onScreen, setOnScreen] = useState(false);
const [image, setImage] = useState(
getImage(imageHash)
);
useEffect(() => {
setImage(getImage(imageHash));
}, [imageHash]);
useEffect(() => {
const top = _me.current.offsetTop
const isOnScreen =
top >= viewPort.top && top <= (viewPort.bottom + _me.current.offsetHeight) ||
(top + _me.current.offsetHeight) >= viewPort.top && (top + _me.current.offsetHeight) <= viewPort.bottom
if (isOnScreen) {
setOnScreen(true)
}
else {
setOnScreen(false)
}
}, [viewPort])
function toggleModal() {
setShowModal(!showModal)
}
function openModal() {
if (showModal) return
setShowModal(true)
}
function getSrc() {
// url encode path
const sanitizedPath = encodeURIComponent(image.path)
return `${baseUrl}:${gradioPort}/cozy-nest/image?path=${sanitizedPath}`;
}
return (
<div id={`img_${index}`} className="image" ref={_me}>
{onScreen ? (<>
<div className="image-wrapper" onClick={openModal}>
<img
className="cozy-nest-thumbnail"
src={getSrc()}
alt="image"
ref={imgRef}/>
</div>
<CozyImageInfo verbose={false} imageHash={imageHash}/>
{showModal && <div className="infoModal">
<div className="image-wrapper">
<img
className="cozy-nest-thumbnail"
src={getSrc()}
alt="image"/>
</div>
<CozyImageInfo verbose={true} imageHash={imageHash} closeModal={toggleModal} />
</div>}
</>) : (<div className="image image-placeholder"/>)}
</div>
);
}

View File

@ -0,0 +1,97 @@
import {Row} from "../main/Utils.jsx";
import {Controls} from "./Controls.jsx";
import React, {useContext, useEffect, useState} from "react";
import Exif from "./editor/ExifEditor.jsx";
import {ImagesContext} from "./ImagesContext.tsx";
import {CozyLogger} from "../main/CozyLogger.js";
import {CozyTags} from "./CozyTags.jsx";
const tryCatch = (fn) => {
try {
return fn()
} catch (ignored) {
return 'Error parsing metadata'
}
}
export function CozyImageInfo({verbose, imageHash, closeModal}) {
const {images, updateExifInState, getImage} = useContext(ImagesContext)
const [image, setImage] = useState(
getImage(imageHash)
);
const [formattedExif, setFormattedExif] = useState({
date: 0,
model: '',
size: '',
seed: '',
steps: '',
sampler: '',
modelHash: '',
formattedAll: ''
})
const isVerbose = verbose;
useEffect(() => {
setImage(getImage(imageHash));
}, [images, imageHash]);
useEffect(() => {
if (!image) return
if (!image.metadata) return
if (!image.metadata.exif) return
if (!image.metadata.exif.parameters) return
setFormattedExif({
date: tryCatch(() => new Date(image.metadata.date * 1000).toISOString().replace(/T/, ' ').replace(/\..+/, '')),
model: tryCatch(() => image.metadata.exif.parameters.split("Model: ")[1].split(",")[0]),
size: tryCatch(() => image.metadata.exif.parameters.split("Size: ")[1].split(",")[0]),
seed: tryCatch(() => image.metadata.exif.parameters.split("Seed: ")[1].split(",")[0]),
steps: tryCatch(() => image.metadata.exif.parameters.split("Steps: ")[1].split(",")[0]),
sampler: tryCatch(() => image.metadata.exif.parameters.split("Sampler: ")[1].split(",")[0]),
modelHash: tryCatch(() => image.metadata.exif.parameters.split("Model hash: ")[1].split(",")[0]),
formattedAll: tryCatch(() => image.metadata.exif.parameters.replace(/\n/g, "<br>"))
})
}, [image])
const close = async () => {
closeModal()
}
if (!image) return (<div className="image-info">No image selected</div>)
return (
<div className="image-info">
{isVerbose &&
<>
<button
className="nevysha lg primary gradio-button btn"
onClick={close}>
Close
</button>
</>
}
<CozyTags imageHash={imageHash} isFull={isVerbose}/>
<table>
<tbody>
<tr><td>Date: </td><td>{formattedExif?.date}</td></tr>
<tr><td>Model: </td><td>{formattedExif?.model}</td></tr>
{isVerbose && <tr>
<td>Model Hash:</td>
<td>{formattedExif?.modelHash}</td>
</tr>}
<tr><td>Size: </td><td>{formattedExif?.size}</td></tr>
<tr><td>Seed: </td><td>{formattedExif?.seed}</td></tr>
<tr><td>Steps: </td><td>{formattedExif?.steps}</td></tr>
<tr><td>Sampler: </td><td>{formattedExif?.sampler}</td></tr>
</tbody>
</table>
{isVerbose && <div className="blocInfo" dangerouslySetInnerHTML={{__html: formattedExif?.formattedAll}}/>}
<Controls imageHash={imageHash}/>
</div>
);
}

View File

@ -0,0 +1,154 @@
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
import {ImagesContext} from "./ImagesContext";
import makeAnimated from 'react-select/animated';
import CreatableSelect from "react-select/creatable";
import Select from 'react-select';
import {saveExif} from "./editor/ExifEditor.jsx";
import {CozyLogger} from "../main/CozyLogger.js";
const animatedComponents = makeAnimated();
// StylesConfig
const styles = {
container: (state) => ({
...state,
width: '100%',
}),
control: (state) => ({
...state,
borderRadius:0,
border: '1px solid var(--border-color-primary)',
background: 'var(--input-background-fill)',
width: '100%',
}),
option: (state) => ({
...state,
borderRadius:0,
color: 'var(--body-text-color)',
background: 'var(--background-fill-primary)',
'&:hover': {
background: 'var(--ae-primary-color)'
}
}),
menu: (state) => ({
...state,
borderRadius:0,
background: 'var(--background-fill-primary)',
border: '1px solid var(--ae-input-border-color) !important'
}),
multiValue: (state) => ({
...state,
borderRadius:0,
background: 'var(--background-fill-primary)',
color: 'var(--nevysha-font-color)',
}),
multiValueLabel: (styles) => ({
...styles,
color: 'var(--nevysha-font-color)',
}),
multiValueRemove: (styles) => ({
...styles,
':hover': {
color: 'white',
},
}),
indicatorContainer: (styles) => ({
...styles,
color: 'var(--nevysha-font-color)',
padding: 0,
})
}
export function CozyTags({imageHash, isFull}) {
const {getImage, tags, setTags} = useContext(ImagesContext)
const [image, setImage] = useState(
getImage(imageHash)
);
const [imgTags, setImgTags] = useState([])
const [isLoading, setIsLoading] = useState(false);
const exifTags = image && image.metadata && image.metadata.exif && image.metadata.exif['cozy-nest-tags']
useEffect(() => {
if (!image) return
if (image.metadata.exif['cozy-nest-tags']) {
const _imgTags = image.metadata.exif['cozy-nest-tags'].split(',')
setImgTags([..._imgTags])
}
else {
setImgTags([])
}
}, [imageHash,exifTags])
useEffect(() => {
setImage(getImage(imageHash))
}, [imageHash])
const handleCreate = (inputValue) => {
setIsLoading(true);
setTags([...tags, inputValue])
//remove duplicates [...imgTags, inputValue]
const _imgTags = [...new Set([...imgTags, inputValue])]
setImgTags([..._imgTags])
setTimeout(() => {
setIsLoading(false);
handleSave([..._imgTags]).then(_ => _)
}, 1000);
};
const handleChange = (newValue) => {
const _newValue = [...new Set(newValue.map(tag => tag.value))]
setImgTags(_newValue);
handleSave(_newValue).then(_ => _)
}
const handleSave = async (newTags) => {
const exif = image.metadata.exif
exif['cozy-nest-tags'] = newTags.join(',')
CozyLogger.debug('Saving tags', exif['cozy-nest-tags'])
await saveExif(image.path, exif)
}
return (
<>
<CreatableSelect
placeholder={'Tags...'}
styles={styles}
isMulti
options={tags.map(tag => ({value: tag, label: tag}))}
onCreateOption={handleCreate}
isDisabled={isLoading}
isLoading={isLoading}
value={imgTags.map(tag => ({value: tag, label: tag}))}
onChange={(tags) => handleChange(tags)}
/>
</>
);
}
export function CozyTagsSelect({setActiveTags}) {
const {tags} = useContext(ImagesContext)
return (
<Select
options={tags.map(tag => ({value: tag, label: tag}))}
components={animatedComponents}
isMulti
placeholder={'Tags...'}
styles={styles}
onChange={(tags) => setActiveTags(tags.map(tag => tag.value))}
/>
)
}

View File

@ -0,0 +1,105 @@
import React, {createContext, ReactNode, useEffect, useState} from 'react';
import {Image} from "../cozy-types";
// @ts-ignore
import {CozyLogger} from "../main/CozyLogger";
interface ImagesContextType {
images: Image[];
setImages: React.Dispatch<React.SetStateAction<Image[]>>;
filteredImages: Image[];
setFilteredImages: React.Dispatch<React.SetStateAction<Image[]>>;
}
export const ImagesContext = createContext<ImagesContextType>({
images: [],
setImages: () => {},
filteredImages: [],
setFilteredImages: () => {}
});
export function ImagesProvider({ children }: { children: ReactNode[] }) {
const [images, setImages] = useState<Image[]>([]);
const [filteredImages, setFilteredImages] = useState<Image[]>([]);
const [tags, setTags] = useState<string[]>([]);
useEffect(() => {
const noDuplicates = [...new Set(tags)];
if (noDuplicates.length !== tags.length) {
setTags(noDuplicates)
}
}, [tags])
const deleteImg = async (what: string, image: Image) => {
const {path} = image
function removeFromImages() {
//remove from images
const newImages = images.filter(image => image.path !== decodeURIComponent(path))
setImages([...newImages])
}
if (what === 'delete') {
const response = await fetch(`/cozy-nest/image?path=${path}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
const json = await response.json()
CozyLogger.debug('json', json)
if (response.ok) {
removeFromImages()
}
}
else if (what === 'archive') {
const response = await fetch(`/cozy-nest/image?path=${path}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({archive: true})
})
const json = await response.json()
CozyLogger.debug('json', json)
if (response.ok) {
removeFromImages()
}
}
}
const updateExifInState = (image: Image) => {
const {metadata: {exif, hash}} = image
const newImages = images.map(image => {
if (image.hash === hash) {
image.metadata.exif = exif
}
return image
})
setImages([...newImages])
}
const getImage = (hash: string) => {
return images.find(image => image.hash === hash)
}
const value = {
images,
setImages,
filteredImages,
setFilteredImages,
deleteImg,
updateExifInState,
getImage,
tags,
setTags
}
return (
<ImagesContext.Provider value={value}>
{children}
</ImagesContext.Provider>
)
}

View File

@ -0,0 +1,34 @@
import {ReadyState} from "react-use-websocket";
import React from 'react'
import Browser from "./Browser.jsx";
import {Column, Row} from "../main/Utils.jsx";
export function MockImageBrowser() {
return (
<>
<Column>
<h1 className="cnib-title">Cozy Nest Image Browser <span className="beta-emphasis">beta</span></h1>
<Row>
<span>The WebSocket is currently <span className="connexionStatus" style={{color:'red'}}>Closed</span></span>
<button
className="nevysha lg primary gradio-button btn"
style={{marginLeft: '20px', width: '410px'}}
disabled={true}
>
Image browser is disabled. To enable it, go to the CozyNest settings.
</button>
</Row>
<textarea data-testid="textbox"
placeholder="Search anything : Prompt, Size, Model, ..."
rows="1"
spellCheck="false"
data-gramm="false"
disabled={true}/>
</Column>
<Browser key={0} imagesRef={[]}/>
</>
);
}

View File

@ -0,0 +1,60 @@
.ExifEditor.backdrop {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
backdrop-filter: blur(5px);
z-index: 101;
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap;
}
.ExifEditor > .container {
display: flex;
flex-direction: column;
width: 1015px;
min-height: 715px;
background-color: var(--ae-input-bg-color);
border: 1px solid var(--ae-input-border-color);
}
.ExifEditor > .container h1 {
font-size: 1.5rem;
padding: 5px 0 5px 0;
margin: 0;
width: 100%;
background-color: var(--ae-input-bg-color);
text-align: center;
border-bottom: 3px solid var(--nevysha-font-color);
}
/*Ace override*/
.ExifEditor .ace_cursor-layer {
z-index: 900;
}
.ExifEditor .ace_cursor-layer .ace_cursor {
z-index: 901;
background-color: var(--ae-primary-color);
position: fixed;
}
@keyframes blink-ace-animate {
from, to { opacity: 0.5; }
60% { opacity: 0; }
}
@keyframes blink-ace-animate-smooth {
from, to { opacity: 0.5; }
45% { opacity: 0.5; }
60% { opacity: 0; }
85% { opacity: 0; }
}
.ExifEditor .ace_active-line {
background: #ffffff0d;
}
.ExifEditor .ace_editor {
background-color: transparent;
}

View File

@ -0,0 +1,127 @@
import React, {useContext, useEffect, useRef, useState} from "react";
import {Button} from "../App.jsx";
import 'ace-builds'
ace.config.setModuleUrl("ace/mode/json_worker", 'cozy-nest-client/node_modules/ace-builds/src-noconflict/worker-json.js')
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github_dark";
import "ace-builds/src-noconflict/ext-language_tools";
import {CozyLogger} from "../../main/CozyLogger.js";
import {ImagesContext} from "../ImagesContext.tsx";
export function ExifEditor({onClose, visible, imageHash}) {
const [exif, setExif] = useState({});
const [exifString, setExifString] = useState('');
const [isJsonValid, setIsJsonValid] = useState(false);
const {images, getImage} = useContext(ImagesContext)
const [image, setImage] = useState(
getImage(imageHash)
);
useEffect(() => {
setImage(getImage(imageHash));
}, [images, imageHash]);
useEffect(() => {
setExif(image.metadata.exif)
setExifString(JSON.stringify(image.metadata.exif, null, 2))
setIsJsonValid(true)
}, [visible, image])
const handleChange = (text) => {
setExifString(text)
try {
const _exif = JSON.parse(text)
setExif(_exif)
setIsJsonValid(true)
} catch (e) {
setIsJsonValid(false)
}
}
const save = async () => {
if (!isJsonValid) {
return
}
const path = image.path
await saveExif(path, exif)
}
const close = () => {
onClose()
}
return (
<>
{visible &&
<div className="ExifEditor backdrop">
<div className="container">
<h1>Exif Editor</h1>
<AceEditor
mode="json"
theme="github_dark"
showPrintMargin={false}
onChange={handleChange}
value={exifString}
name="ace-json-editor"
editorProps={{ $blockScrolling: true }}
style={{width: "100%", height: "100%", zIndex: 200}}
setOptions={{
enableSnippets: true,
cursorStyle: "smooth",
behavioursEnabled: true,
wrapBehavioursEnabled: true,
autoScrollEditorIntoView: true,
wrap: true,
}}
/>
<Button disabled={!isJsonValid} onClick={save}>{isJsonValid ? "Save" : "Invalid JSON"}</Button>
<Button onClick={() => close()}>Close</Button>
</div>
</div>
}
{!visible &&
<div/>
}
</>
);
}
export async function saveExif(path, exif) {
// check if path is URL encoded
if (path.indexOf('%') !== -1) {
path = decodeURIComponent(path)
}
//check if exif['cozy-nest-tags'] contains only ',' or only any number of ','
if (exif['cozy-nest-tags'] && exif['cozy-nest-tags'].match(/^,+$/g)) {
exif['cozy-nest-tags'] = ''
}
const response = await fetch(`/cozy-nest/image-exif`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
//decode path from url
path: path,
data: exif
})
})
const json = await response.json()
CozyLogger.debug('json', json)
}
export default {
save: saveExif,
ExifEditor: ExifEditor
}

View File

@ -0,0 +1,69 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -4,11 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<script type="module" crossorigin src="/file=extensions/Cozy-Nest/cozy-nest-image-browser/assets/index.js"></script>
<link rel="stylesheet" href="/file=extensions/Cozy-Nest/cozy-nest-image-browser/assets/index.css">
</head>
<body>
<div id="cozy-img-browser-react"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -0,0 +1,26 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import {ImagesProvider} from "./ImagesContext.tsx";
import {ChakraProvider} from '@chakra-ui/react'
import {theme} from "../chakra/chakra-theme.ts";
export function startCozyNestImageBrowser() {
if (!document.getElementById('cozy-img-browser-react')) {
setTimeout(() => startCozyNestImageBrowser(), 200)
return
}
ReactDOM.createRoot(document.getElementById('cozy-img-browser-react')).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<ImagesProvider>
<App />
</ImagesProvider>
</ChakraProvider >
</React.StrictMode>,
)
}

View File

@ -7,6 +7,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
<script type="module" src="/main.jsx"></script>
</body>
</html>

View File

@ -1,22 +1,46 @@
import 'animate.css';
import '@fontsource-variable/caveat';
import sheet from './main/cozy-nest-style.css?inline' assert { type: 'css' };
import cozyNestModuleLoader, {fetchCozyNestConfig} from './main/nevysha-cozy-nest.js'
import SimpleTimer from "./main/SimpleTimer.js";
import {COZY_NEST_GRADIO_LOAD_DURATION} from "./main/Constants.js";
import {CozyLogger} from "./main/CozyLogger.js";
import {startCozyNestImageBrowser} from "@image-browser/main.jsx";
import startCozyNestSettings from "@settings/main.jsx";
import {
dummyLoraCard, dummyControlNetBloc, dummySubdirs
} from './main/cozy-utils.js';
import startCozyPrompt from "./cozy-prompt/main.jsx";
import {startExtraNetwork} from "./extra-network/main.jsx";
import Loading from "./main/Loading.js";
window.CozyTools = {
dummyLoraCard,
dummyControlNetBloc,
dummySubdirs
}
import cozyNestLoader from './main/nevysha-cozy-nest.js'
import SimpleTimer from "./main/SimpleTimer.js";
import {COZY_NEST_GRADIO_LOAD_DURATION} from "./main/Constants.js";
import {CozyLogger} from "./main/CozyLogger.js";
export default async function cozyNestLoader() {
await fetchCozyNestConfig();
await cozyNestModuleLoader(async () => {
startCozyNestSettings();
if (COZY_NEST_CONFIG.enable_cozy_prompt === true) {
startCozyPrompt('txt2img_prompt', 'cozy_nest_prompt_txt2img');
startCozyPrompt('img2img_prompt', 'cozy_nest_prompt_img2img');
}
if (COZY_NEST_CONFIG.enable_extra_network_tweaks === true) {
await startExtraNetwork('txt2img');
await startExtraNetwork('img2img');
}
startCozyNestImageBrowser();
});
}
window.cozyNestLoader = cozyNestLoader;
(async () => {
//check if the param CozyNest=No is present in the url
@ -37,15 +61,6 @@ import {CozyLogger} from "./main/CozyLogger.js";
SimpleTimer.time(COZY_NEST_GRADIO_LOAD_DURATION);
// Cozy-Nest-Image-Browser link
const cozyNestImageBrowserLink = document.createElement('link');
cozyNestImageBrowserLink.rel = 'stylesheet';
cozyNestImageBrowserLink.type = 'text/css';
cozyNestImageBrowserLink.href = `file=extensions/Cozy-Nest/cozy-nest-image-browser/assets/index.css?t=${Date.now()}`;
// Append the link element to the document head
document.head.appendChild(cozyNestImageBrowserLink);
if (import.meta.env.VITE_CONTEXT === 'DEV') {
CozyLogger.debug('DEV MODE');
document.addEventListener("DOMContentLoaded", function() {

View File

@ -1,5 +1,3 @@
import cozyNestLoader from "./nevysha-cozy-nest.js";
export class CozyLogger {
static _instance = null;

View File

@ -1,7 +1,7 @@
import {getTheme} from "./cozy-utils.js";
import SimpleTimer from "./SimpleTimer.js";
import {COZY_NEST_GRADIO_LOAD_DURATION} from "./Constants.js";
import {waves, loading_roll} from "./svg.js";
import {waves, loading_ellipsis} from "./svg.js";
import {applyAccentColor, applyBgGradiantColor, applyWavesColor, applyFontColor} from "./tweaks/various-tweaks.js";
export default class Loading {
@ -19,8 +19,8 @@ export default class Loading {
if (Loading._instance) {
Loading._instance.observer.disconnect();
}
//wait for one second to let gradio finish request...
setTimeout(() => document.querySelector("#nevysha-loading-wrap").remove(), 2000);
document.querySelector("#nevysha-loading-wrap").remove();
}
constructor() {
@ -76,7 +76,7 @@ export default class Loading {
<div class="nevysha-cozy-nest-app-name animate__animated animate__backInLeft">
Cozy Nest
</div>
${loading_roll}
${loading_ellipsis}
<div id="loading_step_estimator" class="subtext3 animate__animated animate__pulse animate__infinite">
1
</div>

View File

@ -1,3 +1,5 @@
import {CozyLogger} from './CozyLogger';
export default class SimpleTimer {
static timers = {};
@ -9,7 +11,9 @@ export default class SimpleTimer {
}
static end(timerName) {
return SimpleTimer.timers[timerName].end();
const elapsedTime = SimpleTimer.timers[timerName].end();
CozyLogger.debug(`SimpleTimer: end ${timerName} in ${elapsedTime}ms`)
return elapsedTime;
}
static last(timerName) {

View File

@ -0,0 +1,9 @@
.flex-column {
display: flex;
flex-direction: column;
}
.flex-row {
display: flex;
flex-direction: row;
width: 100%;
}

View File

@ -0,0 +1,32 @@
import React from 'react'
import './Utils.css'
//component to wrap flex row
export function Row(props) {
// if props.className is set, append flex-row to it
// otherwise, set className to flex-row
const className = props.className ? props.className + ' flex-row' : 'flex-row'
return <div
{...props}
className={className}>
{props.children}
</div>
}
export const RowFullWidth = (props) => {
return <Row {...props} style={{width: '100%', justifyContent: 'space-between', gap: '30px'}}/>
}
//component to wrap flex column
export function Column(props) {
const className = props.className ? props.className + ' flex-column' : 'flex-column'
return <div
{...props}
className={className}>
{props.children}
</div>
}

View File

@ -0,0 +1,6 @@
console.log('injecting react-refresh')
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true

View File

@ -529,7 +529,6 @@ input[type=range]::-webkit-slider-thumb {
border-radius: var(--button-large-radius);
padding: var(--button-large-padding);
font-weight: var(--button-large-text-weight);
font-size: var(--button-large-text-size);
--checkbox-background-color: var(--neutral-800);
@ -930,6 +929,9 @@ button.secondary, button.primary {
padding: 10px;
margin-left: 15px;
}
#cozy-img-browser_panel {
border: 1px solid var(--ae-input-border-color) !important;
}
.vertical-line {
width: 2px !important;
@ -987,6 +989,7 @@ canvas.nevysha {
height: calc(100% - (100px + var(--menu-top-height)));
top: calc(75px + var(--menu-top-height));
padding-right: 15px;
border: 1px solid var(--ae-input-border-color) !important;
}
#txt2img_extra_networks_nevysha_wrapper,
#img2img_extra_networks_nevysha_wrapper {
@ -998,7 +1001,7 @@ canvas.nevysha {
#txt2img_extra_networks_nevysha_wrapper > .vertical-line-wrapper,
#img2img_extra_networks_nevysha_wrapper > .vertical-line-wrapper,
.slide-right-browser-panel > .vertical-line-wrapper {
z-index: 9999;
z-index: 100;
margin: 15px 0 0 5px;
}
/*div[id$="_extra_tabs"] {*/
@ -1141,6 +1144,8 @@ input[type="number"] {
margin: 15px 0 15px 0 !important;
border-left: 4px solid var(--ae-primary-color);
padding-left: 5px;
display: flex;
flex-wrap: wrap;
}
.nevysha.settings-nevyui-top > .nevysha-reporting > a {
color: var(--ae-primary-color);
@ -1558,6 +1563,62 @@ textarea.nevysha-image-browser-folder {
transform: rotate(360deg);
}
}
.lds-ellipsis {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ellipsis div {
position: absolute;
top: 33px;
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--ae-primary-color);
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
#kofi_nevysha_support > img {
height: 15px !important;
@ -1799,10 +1860,18 @@ body.nevysha-light .nevysha-button:hover {
fill: var(--body-text-color);
}
#nevyui_update_info_close_btn, #nevyui_update_btn {
width: 60px;
}
#cozynest_others_settings_header > p:nth-child(1) {
font-style: italic;
}
#nevysha_cozy_nest {
display: none;
}
body.nsfw img:not([id='kofi_nevysha_support_img']) {
filter: blur(20px);
}

View File

@ -17,7 +17,7 @@ import Loading from "./Loading.js";
import {waves, svg_magic_wand, svg_update_info} from "./svg.js";
import {
applyAccentColor, applyBgGradiantColor, applyWavesColor
, wrapDataGenerationInfo, wrapSettings, createVerticalLineComp, applyFontColor
, wrapDataGenerationInfo, wrapSettings, createVerticalLineComp, applyFontColor, recalcOffsetFromMenuHeight
} from "./tweaks/various-tweaks.js";
import kofiCup from './kofi-cup-border.png'
import {
@ -213,197 +213,6 @@ function addScrollable(bundle) {
document.getElementById(`${bundle.prefix}_gallery_container`).classList.add("nevysha","nevysha-scrollable")
}
function getHexColorForAccent() {
return document.querySelector("#setting_nevyui_accentColor").querySelector("input").value;
}
function applyCozyNestConfig() {
//waves
const setWaveColor = () => {
const hexColor = document.querySelector("#setting_nevyui_waveColor").querySelector("input").value;
applyWavesColor(hexColor);
}
setWaveColor()
document.querySelector("#setting_nevyui_waveColor").querySelector("input").addEventListener("change", setWaveColor)
//font color
const fontColorInput =
getTheme() === "dark" ?
document.querySelector("#setting_nevyui_fontColor").querySelector("input") :
document.querySelector("#setting_nevyui_fontColorLight").querySelector("input")
//remove hidden css class of parent.parent
fontColorInput.parentElement.parentElement.style.display = "block";
const setFontColor = () => {
const hexColor = fontColorInput.value;
if (!hexColor) return;
applyFontColor(hexColor);
}
setFontColor()
fontColorInput.addEventListener("change", setFontColor)
//background gradient
const setGradientColor = () => {
const hexColor = document.querySelector("#setting_nevyui_bgGradiantColor").querySelector("input").value;
applyBgGradiantColor(hexColor);
}
setGradientColor()
document.querySelector("#setting_nevyui_bgGradiantColor").querySelector("input").addEventListener("change", setGradientColor)
//disable waves and gradiant
const setDisabledWavesAndGradiant = () => {
const disableWavesAndGradiant = document.querySelector("#setting_nevyui_disableWavesAndGradiant").querySelector("input").checked;
const $waves = $('.wave');
const $body = $('body');
if (disableWavesAndGradiant) {
$waves.css('animation', 'none');
$body.css('animation', 'none');
$body.css('background-position', '75% 75%')
}
else {
$waves.css('animation', '');
$body.css('animation', '');
$body.css('background-position', '')
}
}
setDisabledWavesAndGradiant()
document.querySelector("#setting_nevyui_disableWavesAndGradiant").querySelector("input").addEventListener("change", setDisabledWavesAndGradiant)
//background gradient
const setAccentColor = () => {
const hexColor = getHexColorForAccent();
applyAccentColor(hexColor, getHexColorForAccent());
}
//accent generate button
const setAccentForGenerate = () => {
const checked = document.querySelector("#setting_nevyui_accentGenerateButton").querySelector("input").checked;
document.querySelectorAll('button[id$="_generate"]').forEach((btn) => {
if (checked) {
let txtColorAppending = "";
if (getLuminance(getHexColorForAccent()) > 0.5) {
txtColorAppending = "color: black !important";
}
btn.setAttribute("style", `background: var(--ae-primary-color) !important; ${txtColorAppending}`);
} else {
btn.setAttribute("style", '');
}
})
}
setAccentColor()
document.querySelector("#setting_nevyui_accentColor").querySelector("input").addEventListener("change", setAccentColor)
document.querySelector("#setting_nevyui_accentColor").querySelector("input").addEventListener("change", setAccentForGenerate)
setAccentForGenerate()
document.querySelector("#setting_nevyui_accentGenerateButton").querySelector("input").addEventListener("change", setAccentForGenerate);
//font size
const setFontSize = () => {
const fontSize = document.querySelector("#setting_nevyui_fontSize").querySelector("input[type=number]").value;
document.querySelector(':root').style.setProperty('--nevysha-text-md', `${fontSize}px`);
recalcOffsetFromMenuHeight()
}
setFontSize()
document.querySelector("#setting_nevyui_fontSize").querySelector("input[type=number]").addEventListener("change", setFontSize)
document.querySelector("#setting_nevyui_fontSize").querySelector("input[type=range]").addEventListener("change", setFontSize)
//card height
const setCardHeight = () => {
const cardHeight = document.querySelector("#setting_nevyui_cardHeight").querySelector("input[type=number]").value;
document.querySelector(':root').style.setProperty('--extra-network-card-height', `${cardHeight}em`);
}
setCardHeight()
document.querySelector("#setting_nevyui_cardHeight").querySelector("input[type=number]").addEventListener("change", setCardHeight)
document.querySelector("#setting_nevyui_cardHeight").querySelector("input[type=range]").addEventListener("change", setCardHeight)
//card width
const setCardWidth = () => {
const cardWidth = document.querySelector("#setting_nevyui_cardWidth").querySelector("input[type=number]").value;
document.querySelector(':root').style.setProperty('--extra-network-card-width', `${cardWidth}em`);
}
setCardWidth()
document.querySelector("#setting_nevyui_cardWidth").querySelector("input[type=number]").addEventListener("change", setCardWidth)
document.querySelector("#setting_nevyui_cardWidth").querySelector("input[type=range]").addEventListener("change", setCardWidth)
//check if menu is in left or top mode
const menuPosition = () => {
const isLeftChecked = document.querySelector("#setting_nevyui_menuPosition").querySelector("input[value=left]").checked;
//top mode
if (!isLeftChecked) {
document.querySelector(".nevysha.nevysha-tabnav").classList.add("menu-fix-top")
document.querySelector(".gradio-container.app").classList.add("menu-fix-top")
document.querySelector("#nevysha-btn-menu-wrapper")?.classList.add("menu-fix-top")
document.querySelector(':root').style.setProperty('--nevysha-margin-left', `0`);
document.querySelector(':root').style.setProperty('--menu-top-height', `25px`);
//centered or not
const isCenteredChecked = document.querySelector("#setting_nevyui_menuPosition").querySelector("input[value=top_centered]").checked;
if (isCenteredChecked) {
COZY_NEST_CONFIG.main_menu_position = "top_centered";
document.querySelector(".nevysha.nevysha-tabnav").classList.add("center-menu-items")
} else {
COZY_NEST_CONFIG.main_menu_position = "top";
document.querySelector(".nevysha.nevysha-tabnav").classList.remove("center-menu-items")
}
}
//left mode
else {
COZY_NEST_CONFIG.main_menu_position = "left";
document.querySelector(".nevysha.nevysha-tabnav").classList.remove("center-menu-items")
document.querySelector(".nevysha.nevysha-tabnav").classList.remove("menu-fix-top")
document.querySelector(".gradio-container.app").classList.remove("menu-fix-top")
document.querySelector("#nevysha-btn-menu-wrapper")?.classList.remove("menu-fix-top")
document.querySelector(':root').style.setProperty('--nevysha-margin-left', `175px`);
document.querySelector(':root').style.setProperty('--menu-top-height', `1px`);
}
recalcOffsetFromMenuHeight()
}
menuPosition()
document.querySelector("#setting_nevyui_menuPosition").querySelector("input[value=left]").addEventListener("change", menuPosition)
document.querySelector("#setting_nevyui_menuPosition").querySelector("input[value=top]").addEventListener("change", menuPosition)
document.querySelector("#setting_nevyui_menuPosition").querySelector("input[value=top_centered]").addEventListener("change", menuPosition)
//quicksetting gap
const setQuicksettingPosition = () => {
const position = document.querySelector("#setting_nevyui_quicksettingsPosition")
.querySelector("input[type=radio]:checked").value;
if (position === 'split') {
document.querySelector("#quicksettings_gap").classList.add("nevysha-quicksettings-gap")
document.querySelector("#quicksettings").classList.remove("centered-quicksettings")
}
else if (position === 'centered') {
document.querySelector("#quicksettings_gap").classList.remove("nevysha-quicksettings-gap")
document.querySelector("#quicksettings").classList.add("centered-quicksettings")
}
else {
document.querySelector("#quicksettings_gap").classList.remove("nevysha-quicksettings-gap")
document.querySelector("#quicksettings").classList.remove("centered-quicksettings")
}
}
setQuicksettingPosition()
document.querySelector("#setting_nevyui_quicksettingsPosition")
.querySelectorAll("input[type=radio]").forEach((input) => input.addEventListener("change", setQuicksettingPosition))
//enable/disable the sfw mode
const setSfwSettings = () => {
const isSfwChecked = document.querySelector("#setting_nevyui_sfwMode").querySelector("input[type=checkbox]").checked;
if (isSfwChecked) {
document.querySelector('body').classList.add("nsfw");
}
else {
document.querySelector('body').classList.remove("nsfw");
}
}
setSfwSettings()
document.querySelector("#setting_nevyui_sfwMode").querySelector("input[type=checkbox]").addEventListener("change", setSfwSettings)
}
function tweakAWQ() {
const observer = new MutationObserver((mutationsList, observer) => {
@ -458,8 +267,6 @@ function tweakAWQ() {
}
});
}
}
const addCozyNestCustomBtn = () => {
@ -468,9 +275,6 @@ const addCozyNestCustomBtn = () => {
nevySettingstabMenuWrapper.classList.add("nevysha-btn-menu-wrapper");
nevySettingstabMenuWrapper.id = "nevysha-btn-menu-wrapper";
//add a new button in the tabnav
const nevySettingstabMenu2 = `<button class="nevysha-btn-menu" id="nevyui_sh_options" title="Nevysha Cozy Nest Settings">${svg_magic_wand}</button>`;
nevySettingstabMenuWrapper.insertAdjacentHTML('beforeend', nevySettingstabMenu2);
//add a new button in the tabnav
const updateInfoBtn = `<button class="nevysha-btn-menu" id="nevyui_update_info" title="Nevysha Cozy Nest Update Info">${svg_update_info}</button>`;
nevySettingstabMenuWrapper.insertAdjacentHTML('beforeend', updateInfoBtn);
@ -485,32 +289,27 @@ const addCozyNestCustomBtn = () => {
updateTab.style = "display: none;";
document.querySelector("#tabs").insertAdjacentElement("beforeend", updateTab)
//add kofi image :blush:
const kofiImg = document.createElement('button')
kofiImg.id = 'kofi_nevysha_support'
kofiImg.innerHTML = `<img id="kofi_nevysha_support_img" height="15" src="${kofiCup}" alt="Consider a donation on ko-fi! :3">`
kofiImg.title = "Consider a donation on ko-fi! :3"
nevySettingstabMenuWrapper.insertAdjacentElement('beforeend', kofiImg);
// add click event to the new update info button
function listenerClosure() {
let shown = false;
document.querySelector("#nevyui_update_info").addEventListener("click", (e) => {
//cancel event
e.preventDefault();
e.stopPropagation();
//create a div that will contain a dialog to display the iframe
const kofiTab = document.createElement("div");
kofiTab.classList.add("nevysha-kofi-tab", "nevysha", "nevysha-tab", "nevysha-tab-settings");
kofiTab.id = "nevyui_kofi_panel";
kofiTab.style = "display: none;";
// kofiTab.innerHTML = `<iframe id='kofiframe' src='https://ko-fi.com/nevysha/?hidefeed=true&widget=true&embed=true&preview=true' style='border:none;width:100%;padding:4px;background:#f9f9f9;' height='712' title='nevysha'></iframe>`
document.querySelector("#tabs").insertAdjacentElement("beforeend", kofiTab)
//show tab_nevyui by default to bypass gradio hidding tabs
document.querySelector("#tab_nevyui").style.display = "block";
let kofiImgIsVisible = false
function toggleKofiPanel() {
window.open("https://ko-fi.com/nevysha", "_blank")
//toggle the panel with a slide animation using jquery
if (shown) {
$("#nevyui_update_info_panel").slideUp(ANIMATION_SPEED);
} else {
$("#nevyui_update_info_panel").slideDown(ANIMATION_SPEED);
}
shown = !shown;
});
}
//add event listener to the button
kofiImg.addEventListener("click", () => {
toggleKofiPanel();
});
listenerClosure();
//fetch version_data.json
loadVersionData().then(ignored => ignored)
@ -614,6 +413,13 @@ function createFolderListComponent() {
const componentContainer = document.querySelector('#cnib_output_folder').parentElement;
const textarea = document.querySelector('#cnib_output_folder textarea');
componentContainer.classList.remove('hidden')
$(componentContainer).css('padding', '0 10px')
//add a label
const label = document.createElement('label');
label.classList.add('nevysha-label');
label.innerHTML = 'Folders to scrap for images';
componentContainer.appendChild(label);
function updateList(foldersList) {
document.querySelectorAll('.nevysha-image-browser-folder-container').forEach(el => el.remove());
@ -707,199 +513,6 @@ function createFolderListComponent() {
parseAndDisplayFolderSettings();
}
const tweakNevyUiSettings = () => {
// select button element with "Nevysha Cozy Nest" as its content
const nevySettingstabMenu = $('#tabs > div > button:contains("Nevysha Cozy Nest")');
// hide the button
nevySettingstabMenu.hide();
addCozyNestCustomBtn();
///create an hideable right side panel
const nevySettingstab = `<div id="nevyui_sh_options_panel" class="nevysha nevysha-tab nevysha-tab-settings" style="display: none;">`;
document.querySelector("#tabs").insertAdjacentHTML('beforeend', nevySettingstab);
//put tab_nevyui inside the panel
document.querySelector("#nevyui_sh_options_panel").appendChild(document.querySelector("#tab_nevyui"));
//add title
const title = `
<div class="nevysha settings-nevyui-title">
<h2>Nevysha's Cozy Nest</h2>
<span class="subtitle">Find your cozy spot on Auto1111's webui</span>
</div>`;
document.querySelector("#nevyui_sh_options_panel").insertAdjacentHTML("afterbegin", title);
//add an event listener on #nevyui_sh_options_submit to briefly show a message when the user clicks on it
document.querySelector("#nevyui_sh_options_submit").addEventListener("click", (e) => {
//cancel event
e.preventDefault();
e.stopPropagation();
//show the message with a smooth animation using jquery
$("#nevysha-saved-feedback").fadeIn();
try {
const jsonFolders = JSON.parse(document.querySelector('#cnib_output_folder textarea').value);
//send config data with POST to /cozy-nest/config
const config = {
"cnib_output_folder": jsonFolders,
}
fetch('/cozy-nest/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
}).then(response => {
if (response.status === 200) {
return response.json();
} else {
throw new Error('Something went wrong on api server!');
}
})
}
catch (e) {
CozyLogger.debug(e);
}
//hide the message after 1.5 second
setTimeout(() => {
$("#nevysha-saved-feedback").fadeOut();
//save new settings in localStorage
(() => fetchCozyNestConfig())() //ignore async warn
}, 1500);
});
//add an event listener on #nevyui_sh_options_submit to briefly show a message when the user clicks on it
document.querySelector("#nevyui_sh_options_reset").addEventListener("click", (e) => {
//cancel event
e.preventDefault();
e.stopPropagation();
//show the message with a smooth animation using jquery
$("#nevysha-reset-feedback").fadeIn();
//hide the message after 1.5 second
setTimeout(() => {
$("#nevysha-reset-feedback").fadeOut();
//save new settings in localStorage
(() => fetchCozyNestConfig())() //ignore async warn
}, 1500);
});
//show tab_nevyui by default to bypass gradio
document.querySelector("#tab_nevyui").style.display = "block";
//add click event to the new settings button
(function closure() {
let shown = false;
document.querySelector("#nevyui_sh_options").addEventListener("click", (e) => {
//cancel event
e.preventDefault();
e.stopPropagation();
//show tab_nevyui by default to bypass gradio hidding tabs
document.querySelector("#tab_nevyui").style.display = "block";
//toggle the panel with a slide animation using jquery
if (shown) {
$("#nevyui_sh_options_panel").slideUp(ANIMATION_SPEED);
} else {
$("#nevyui_sh_options_panel").slideDown(ANIMATION_SPEED);
}
shown = !shown;
});
})();
//add click event to the new update info button
(function closure() {
let shown = false;
document.querySelector("#nevyui_update_info").addEventListener("click", (e) => {
//cancel event
e.preventDefault();
e.stopPropagation();
//show tab_nevyui by default to bypass gradio hidding tabs
document.querySelector("#tab_nevyui").style.display = "block";
//toggle the panel with a slide animation using jquery
if (shown) {
$("#nevyui_update_info_panel").slideUp(ANIMATION_SPEED);
} else {
$("#nevyui_update_info_panel").slideDown(ANIMATION_SPEED);
}
shown = !shown;
});
})();
createFolderListComponent();
}
const makeSettingsDraggable = () => {
// Get a reference to the draggable div element
const draggableSettings = document.querySelector('#nevyui_sh_options_panel');
// Define variables to keep track of the mouse position and offset
let isDragging = false;
let mouseX = 0;
let mouseY = 0;
let offsetX = 0;
let offsetY = 0;
// create draggable icon
const draggableAnchorIcon = document.createElement('div');
draggableAnchorIcon.classList.add('nevysha-draggable-anchor-icon', 'nevysha-button');
//add a drag icon
draggableAnchorIcon.innerHTML = "Drag Me";
// add the anchor to the start of the draggable div
draggableSettings.insertBefore(draggableAnchorIcon, draggableSettings.firstChild);
// create a blank div above the svg icon to catch for mousedown events
const draggableAnchor = document.createElement('div');
draggableAnchor.classList.add('nevysha-draggable-anchor', 'nevysha-button');
// add the anchor to the start of the draggable div
draggableSettings.insertBefore(draggableAnchor, draggableSettings.firstChild);
//add close button
const settingCloseButton = document.createElement('div');
settingCloseButton.classList.add('nevysha-draggable-anchor', 'nevysha-draggable-anchor-icon', 'nevysha-setting-close-button', 'nevysha-button');
settingCloseButton.innerHTML = 'Close';
settingCloseButton.style.left = '70px';
settingCloseButton.style.top = '2px';
draggableSettings.appendChild(settingCloseButton);
settingCloseButton.addEventListener('click', () => {
document.querySelector("#nevyui_sh_options").click();
});
// Add event listeners for mouse events
draggableAnchor.addEventListener('mousedown', function(event) {
// Set dragging flag and store mouse position and offset from element top-left corner
isDragging = true;
mouseX = event.clientX;
mouseY = event.clientY;
offsetX = draggableSettings.offsetLeft;
offsetY = draggableSettings.offsetTop;
});
document.addEventListener('mousemove', function(event) {
// If dragging, update element position based on mouse movement
if (isDragging) {
const deltaX = event.clientX - mouseX;
const deltaY = event.clientY - mouseY;
draggableSettings.style.left = (offsetX + deltaX) + 'px';
draggableSettings.style.top = (offsetY + deltaY) + 'px';
}
});
document.addEventListener('mouseup', function(event) {
// Reset dragging flag
isDragging = false;
});
}
function observeElementAdded(targetSelector, callback) {
// Create a new MutationObserver instance
@ -966,12 +579,12 @@ function tweakExtraNetworks({prefix}) {
} else {
//hide the extra network
$(extraNetworkGradioWrapper).animate({
"margin-right": `-=${extraNetworkGradioWrapper.offsetWidth}`},
ANIMATION_SPEED,
() => {
// hide it after the animation is done
extraNetworkGradioWrapper.style.display = 'none';
});
"margin-right": `-=${extraNetworkGradioWrapper.offsetWidth}`},
ANIMATION_SPEED,
() => {
// hide it after the animation is done
extraNetworkGradioWrapper.style.display = 'none';
});
}
}
@ -1297,32 +910,18 @@ const addTabWrapper = () => {
}
function createRightWrapperDiv() {
const tab = document.querySelector(`div#tabs`);
//create wrapper div for the button
const rightPanBtnWrapper = document.createElement('div');
rightPanBtnWrapper.setAttribute('id', `right_button_wrapper`);
rightPanBtnWrapper.classList.add('nevysha', 'nevysha-right-button-wrapper');
//add button to the begining of the tab
tab.insertAdjacentElement('beforeend', rightPanBtnWrapper);
//add a button for image browser
function buildRightSlidePanelFor(label, buttonLabel, rightPanBtnWrapper, tab) {
const cozyImgBrowserBtn = document.createElement('button');
cozyImgBrowserBtn.setAttribute('id', `image_browser_right_button`);
cozyImgBrowserBtn.setAttribute('id', `${label}_right_button`);
cozyImgBrowserBtn.classList.add('nevysha', 'lg', 'primary', 'gradio-button');
cozyImgBrowserBtn.innerHTML = `<div>Cozy Image Browser</div>`;
cozyImgBrowserBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
});
cozyImgBrowserBtn.innerHTML = `<div>${buttonLabel}</div>`;
rightPanBtnWrapper.appendChild(cozyImgBrowserBtn);
//create a panel to display Cozy Image Browser
const cozyImgBrowserPanel =
`<div id="cozy_img_browser_panel" class="nevysha cozy-img-browser-panel slide-right-browser-panel" style="display: none">
`<div id="${label}_panel" class="nevysha slide-right-browser-panel" style="display: none">
<div class="nevysha slide-right-browser-panel-container nevysha-scrollable">
<div class="nevysha" id="cozy-img-browser-react"/>
<div class="nevysha" id="${label}-react"/>
</div>
</div>`;
//add the panel to the end of the tab
@ -1330,9 +929,9 @@ function createRightWrapperDiv() {
// Create a vertical line component
const lineWrapper = createVerticalLineComp();
const cozyImgBrowserPanelWrapper = document.querySelector('#cozy_img_browser_panel');
const cozyImgBrowserPanelWrapper = document.querySelector(`#${label}_panel`);
//set cozyImgBrowserPanelWrapper.style.width from local storage value if it exists
const cozyImgBrowserPanelWidth = localStorage.getItem('cozyImgBrowserPanelWrapper');
const cozyImgBrowserPanelWidth = localStorage.getItem(`${label}_panelWidth`);
if (cozyImgBrowserPanelWidth) {
cozyImgBrowserPanelWrapper.style.width = cozyImgBrowserPanelWidth;
}
@ -1341,7 +940,7 @@ function createRightWrapperDiv() {
//TODO refactor to factorise code bellow with extraNetwork
//add a close button inside the line
const closeCozyImgBrowser = document.createElement('button');
closeCozyImgBrowser.setAttribute('id', `floating_close_cozy_img_browser_panel_button`);
closeCozyImgBrowser.setAttribute('id', `floating_close_${label}__panel_button`);
//add button class
closeCozyImgBrowser.classList.add('nevysha', 'lg', 'primary', 'gradio-button', 'nevysha-extra-network-floating-btn');
closeCozyImgBrowser.innerHTML = '<div>Close</div>';
@ -1352,7 +951,7 @@ function createRightWrapperDiv() {
//add the button at the begining of the div
lineWrapper.insertBefore(closeCozyImgBrowser, lineWrapper.firstChild);
//Add an event listener to the resizer element to track mouse movement
lineWrapper.addEventListener('mousedown', function(e) {
lineWrapper.addEventListener('mousedown', function (e) {
e.preventDefault();
// Set the initial values for the width and height of the container
@ -1376,7 +975,7 @@ function createRightWrapperDiv() {
function stopDrag() {
//save the new width in local storage
localStorage.setItem(`cozyImgBrowserPanelWrapper`, cozyImgBrowserPanelWrapper.style.width);
localStorage.setItem(`${label}_panelWidth`, cozyImgBrowserPanelWrapper.style.width);
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
@ -1384,16 +983,15 @@ function createRightWrapperDiv() {
});
//add listener to open or close the panel using jquery animate
cozyImgBrowserBtn.addEventListener('click', (e) => {
cozyImgBrowserBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const panel = document.querySelector('#cozy_img_browser_panel');
const panel = document.querySelector(`#${label}_panel`);
if (panel.style.display === 'none') {
panel.style.display = 'flex'
panel.style.marginRight = `-${panel.offsetWidth}px`;
$(panel).animate({"margin-right": `+=${panel.offsetWidth}`}, ANIMATION_SPEED);
}
else {
} else {
$(panel).animate({"margin-right": `-=${panel.offsetWidth}`}, ANIMATION_SPEED, () => {
panel.style.display = 'none'
});
@ -1401,19 +999,40 @@ function createRightWrapperDiv() {
});
}
function setButtonVisibilityFromCurrentTab(id) {
function createRightWrapperDiv() {
const tab = document.querySelector(`div#tabs`);
//hide each button that ends with extra_networks_right_button
const extraNetworksRightBtns = document.querySelectorAll(`button[id$="extra_networks_right_button"]`);
extraNetworksRightBtns.forEach((btn) => {
btn.style.display = 'none';
//create wrapper div for the button
const rightPanBtnWrapper = document.createElement('div');
rightPanBtnWrapper.setAttribute('id', `right_button_wrapper`);
rightPanBtnWrapper.classList.add('nevysha', 'nevysha-right-button-wrapper');
//add button to the begining of the tab
tab.insertAdjacentElement('beforeend', rightPanBtnWrapper);
})
if (id === 'tab_txt2img') {
document.querySelector('button#txt2img_extra_networks_right_button').style.display = 'flex';
if (COZY_NEST_CONFIG.enable_extra_network_tweaks === true) {
buildRightSlidePanelFor('cozy-txt2img-extra-network', 'Extra Network', rightPanBtnWrapper, tab);
document.getElementById('cozy-txt2img-extra-network-react').classList.add('cozy-extra-network')
buildRightSlidePanelFor('cozy-img2img-extra-network', 'Extra Network', rightPanBtnWrapper, tab);
document.getElementById('cozy-img2img-extra-network-react').classList.add('cozy-extra-network')
document.querySelector(`#cozy-img2img-extra-network_right_button`).style.display = 'none';
}
if (id === 'tab_img2img') {
document.querySelector('button#img2img_extra_networks_right_button').style.display = 'flex';
if (COZY_NEST_CONFIG.disable_image_browser !== true) {
buildRightSlidePanelFor('cozy-img-browser', 'Cozy Image Browser', rightPanBtnWrapper, tab);
}
}
function setButtonVisibilityFromCurrentTab(id) {
CozyLogger.debug(`setButtonVisibilityFromCurrentTab(${id})`);
document.querySelector(`#cozy-txt2img-extra-network_right_button`).style.display = 'none';
document.querySelector(`#cozy-img2img-extra-network_right_button`).style.display = 'none';
if (id === 'tab_txt2img') {
document.querySelector(`#cozy-txt2img-extra-network_right_button`).style.display = 'flex';
}
else if (id === 'tab_img2img') {
document.querySelector(`#cozy-img2img-extra-network_right_button`).style.display = 'flex';
}
}
@ -1484,68 +1103,6 @@ window.sendToPipe = sendToPipe;
window.troubleshootSize = {}
const recalcOffsetFromMenuHeight = () => {
let menuHeight = 0;
const tabs = document.getElementById('tabs');
const footer = document.querySelector('#footer #footer');
let footerHeight;
if (!footer) {
if (COZY_NEST_CONFIG.webui === WEBUI_SDNEXT)
footerHeight = 5;
else
footerHeight = 0;
}
else {
footerHeight = footer.offsetHeight;
}
if (COZY_NEST_CONFIG.main_menu_position !== 'left') {
const menu = document.querySelector('.tab-nav.nevysha-tabnav')
menuHeight = menu.offsetHeight + 2;
document.querySelector(':root').style.setProperty('--menu-top-height', `${menuHeight}px`);
const $app = $('.gradio-container.app');
$app.attr('style', `${$app.attr('style')} padding-top: ${menuHeight}px !important;`);
const rect = tabs.getBoundingClientRect();
const tabsTop = rect.top;
document.querySelector(':root').style.setProperty('--main-container-height', `${window.innerHeight - (tabsTop + footerHeight)}px`);
window.troubleshootSize = {
menuHeight,
footerHeight: footerHeight,
tabsTop,
WindowInnerHeight: window.innerHeight,
bodyHeight: window.innerHeight - (tabsTop + footerHeight),
'main-container-height': `${window.innerHeight - (tabsTop + footerHeight)}px`,
}
}
else {
document.querySelector(':root').style.setProperty('--menu-top-height', `1px`);
const $app = $('.gradio-container.app');
$app.attr('style', `${$app.attr('style')} padding-top: ${menuHeight}px !important;`);
const rect = tabs.getBoundingClientRect();
const tabsTop = rect.top;
document.querySelector(':root').style.setProperty('--main-container-height', `${window.innerHeight - (tabsTop + footerHeight)}px`);
window.troubleshootSize = {
menuHeight,
footerHeight: footerHeight,
tabsTop,
WindowInnerHeight: window.innerHeight,
bodyHeight: window.innerHeight - (tabsTop + footerHeight),
'main-container-height': `${window.innerHeight - (tabsTop + footerHeight)}px`,
}
}
}
function addOptionsObserver() {
// Select the target node
const targetNode = document.body;
@ -1616,10 +1173,6 @@ const onloadSafe = (done) => {
// }
}
function tweakForSDNext() {
document.querySelector('#setting_nevyui_fetchOutputFolderFromA1111Settings').style.display = 'none';
}
const onLoad = (done) => {
let gradioApp = window.gradioApp;
@ -1675,7 +1228,7 @@ const onLoad = (done) => {
document.querySelectorAll('.extra-network-cards').forEach(elem => elem.setAttribute('class', `${elem.getAttribute('class')} nevysha nevysha-scrollable`))
document.querySelectorAll('#cozy_nest_settings_tabs > .tabitem').forEach(elem => elem.classList.add('nevysha', 'nevysha-scrollable'))
document.querySelector('#nevyui_sh_options_start_socket').setAttribute('style', 'display: none;')
//hide "send to" panel in settings
//this panel is used to transfert image data into tab
document.querySelector('#nevysha-send-to').setAttribute('style', 'display: none;')
@ -1697,23 +1250,13 @@ const onLoad = (done) => {
addDraggable(bundle);
addScrollable(bundle);
if (COZY_NEST_CONFIG.enable_extra_network_tweaks) {
tweakExtraNetworks(bundle);
addExtraNetworksBtn(bundle);
}
//add a clear button to generated image
clearGeneratedImage(bundle);
}
if (COZY_NEST_CONFIG.enable_extra_network_tweaks) {
document.querySelector(`button#txt2img_extra_networks`).click();
document.querySelector(`button#img2img_extra_networks`).click();
}
setTimeout(() => {
nevysha_magic({prefix: "txt2img"});
nevysha_magic({prefix: "img2img"});
}, 500)
nevysha_magic({prefix: "txt2img"});
nevysha_magic({prefix: "img2img"});
//general
tweakButtonsIcons();
@ -1725,11 +1268,9 @@ const onLoad = (done) => {
//add expend to inpainting
tweakInpainting();
//tweak webui setting page for Cozy Nest directly with JS because... gradio blblblbl
tweakNevyUiSettings();
addCozyNestCustomBtn();
//load settings
applyCozyNestConfig();
recalcOffsetFromMenuHeight();
//add tab wrapper
@ -1744,19 +1285,9 @@ const onLoad = (done) => {
document.querySelector("body").classList.remove("nevysha-light")
}
//make settings draggable
makeSettingsDraggable();
//add observer for .options resize
addOptionsObserver();
if (COZY_NEST_CONFIG.webui === WEBUI_SDNEXT) {
tweakForSDNext();
}
//load /assets/index-eff6a2cc.js
loadCozyNestImageBrowserSubmodule();
/* --------------- TWEAK SOME EXTENSION --------------- */
//if AWQ-container is present in COZY_NEST_CONFIG.extensions array from localStorage, tweak AWQ
if (COZY_NEST_CONFIG.extensions
@ -1768,15 +1299,17 @@ const onLoad = (done) => {
done();
};
async function loadCozyNestImageBrowserSubmodule() {
try {
const jsModule = await fetch(`file=extensions/Cozy-Nest/cozy-nest-image-browser/assets/index.js?t=${Date.now()}`);
eval(await jsModule.text());
}
catch (err) {
// handle any errors that occur during the import process
console.error("Failed to load cozy-nest-image-browser submodule", err);
}
export async function saveCozyNestConfig(config) {
config = config || COZY_NEST_CONFIG;
await fetch('/cozy-nest/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
})
}
async function detectWebuiContext() {
@ -1794,13 +1327,7 @@ async function detectWebuiContext() {
COZY_NEST_CONFIG.webui = WEBUI_A1111;
}
await fetch('/cozy-nest/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(COZY_NEST_CONFIG)
})
await saveCozyNestConfig();
}
CozyLogger.debug(`webui is ${COZY_NEST_CONFIG.webui}`)
}
@ -1810,7 +1337,7 @@ async function detectWebuiContext() {
* either from main.js for Dev or from the loader in the extension folder
* @returns {Promise<void>}
*/
export default async function cozyNestLoader() {
export default async function cozyNestModuleLoader(extraCozyNestModulesLoader) {
//check if the param CozyNest=No is present in the url
const urlParams = new URLSearchParams(window.location.search);
@ -1834,22 +1361,28 @@ export default async function cozyNestLoader() {
setupPopupInstanceInfo();
setupErrorHandling();
onloadSafe(() => {
CozyLogger.log(`running.`);
//remove #nevysha-loading from DOM
Loading.stop();
// wrap onloadSafe in Promise
return new Promise(resolve => {
onloadSafe(async () => {
SimpleTimer.end(COZY_NEST_DOM_TWEAK_LOAD_DURATION);
SimpleTimer.end(COZY_NEST_GRADIO_LOAD_DURATION);
await extraCozyNestModulesLoader();
if (shouldDisplaySDNextWarning)
showAlert(
"Warning",
"Cozy Nest detected that you are using SD.Next and running Cozy Nest for the first time. To ensure compatibility, please restart the server."
)
});
//remove #nevysha-loading from DOM
Loading.stop();
CozyLogger.log(`running.`);
SimpleTimer.end(COZY_NEST_DOM_TWEAK_LOAD_DURATION);
SimpleTimer.end(COZY_NEST_GRADIO_LOAD_DURATION);
if (shouldDisplaySDNextWarning)
showAlert(
"Warning",
"Cozy Nest detected that you are using SD.Next and running Cozy Nest for the first time. To ensure compatibility, please restart the server."
)
resolve();
});
})
};
window.cozyNestLoader = cozyNestLoader;
function setupErrorHandling() {
@ -1857,26 +1390,28 @@ function setupErrorHandling() {
//set a global error handler
window.addEventListener('error', function ({message, filename , lineno, colno, error }) {
// get setting_nevyui_errorPopup checkbox value
const errorPopup = document.querySelector('#setting_nevyui_errorPopup').querySelector("input").checked;
if (!errorPopup) return;
//TODO uncomment
//if filename does not contains Cozy-Nest, ignore
if (!filename.toLowerCase().includes('cozy-nest')) return;
// Handle the error here
populateInstanceInfoDialog();
document.querySelector('#cozy_nest_error_handling_display').innerHTML = `An error occurred: ${message} at ${filename } line ${lineno} column ${colno}`;
document.querySelector('#cozy_nest_error_handling_display_stack').innerHTML = error.stack;
document.querySelector('#cozy_nest_error_handling_display_stack').setAttribute('style', 'display: block;');
showInstanceInfoDialog();
// // get setting_nevyui_errorPopup checkbox value
// const errorPopup = document.querySelector('#setting_nevyui_errorPopup').querySelector("input").checked;
// if (!errorPopup) return;
//
// //if filename does not contains Cozy-Nest, ignore
// if (!filename.toLowerCase().includes('cozy-nest')) return;
//
// // Handle the error here
// populateInstanceInfoDialog();
// document.querySelector('#cozy_nest_error_handling_display').innerHTML = `An error occurred: ${message} at ${filename } line ${lineno} column ${colno}`;
// document.querySelector('#cozy_nest_error_handling_display_stack').innerHTML = error.stack;
// document.querySelector('#cozy_nest_error_handling_display_stack').setAttribute('style', 'display: block;');
// showInstanceInfoDialog();
});
}
let COZY_NEST_CONFIG;
let shouldDisplaySDNextWarning = false;
async function fetchCozyNestConfig() {
export async function fetchCozyNestConfig() {
const response = await fetch(`file=extensions/Cozy-Nest/nevyui_settings.json?t=${Date.now()}`);
if (response.ok) {
COZY_NEST_CONFIG = await response.json();

View File

@ -2,4 +2,5 @@ export const waves = "<div id='nevy_waves'><div class='wave'></div> <div class='
export const svg_magic_wand = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M234.7 42.7L197 56.8c-3 1.1-5 4-5 7.2s2 6.1 5 7.2l37.7 14.1L248.8 123c1.1 3 4 5 7.2 5s6.1-2 7.2-5l14.1-37.7L315 71.2c3-1.1 5-4 5-7.2s-2-6.1-5-7.2L277.3 42.7 263.2 5c-1.1-3-4-5-7.2-5s-6.1 2-7.2 5L234.7 42.7zM46.1 395.4c-18.7 18.7-18.7 49.1 0 67.9l34.6 34.6c18.7 18.7 49.1 18.7 67.9 0L529.9 116.5c18.7-18.7 18.7-49.1 0-67.9L495.3 14.1c-18.7-18.7-49.1-18.7-67.9 0L46.1 395.4zM484.6 82.6l-105 105-23.3-23.3 105-105 23.3 23.3zM7.5 117.2C3 118.9 0 123.2 0 128s3 9.1 7.5 10.8L64 160l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L128 160l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L128 96 106.8 39.5C105.1 35 100.8 32 96 32s-9.1 3-10.8 7.5L64 96 7.5 117.2zm352 256c-4.5 1.7-7.5 6-7.5 10.8s3 9.1 7.5 10.8L416 416l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L480 416l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L480 352l-21.2-56.5c-1.7-4.5-6-7.5-10.8-7.5s-9.1 3-10.8 7.5L416 352l-56.5 21.2z"/></svg>`;
export const svg_update_info = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-167l80 80c9.4 9.4 24.6 9.4 33.9 0l80-80c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-39 39V184c0-13.3-10.7-24-24-24s-24 10.7-24 24V318.1l-39-39c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9z"/></svg>`;
export const loading_roll = `<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>`
export const loading_ellipsis = `<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>`
export const xmark = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>`

View File

@ -1,6 +1,9 @@
import {getLuminance, getSubduedFontColor, hexToRgb} from "../cozy-utils.js";
import $ from "jquery";
import {WEBUI_SDNEXT} from "../Constants.js";
export function applyWavesColor(hexColor) {
COZY_NEST_CONFIG.waves_color = hexColor;
const rgbColor = hexToRgb(hexColor);
document.querySelectorAll(".wave").forEach((wave) => {
wave.setAttribute("style", `background: rgb(${rgbColor} / 16%)`);
@ -8,17 +11,20 @@ export function applyWavesColor(hexColor) {
}
export function applyFontColor(hexColor) {
COZY_NEST_CONFIG.font_color = hexColor;
const rgbColor = hexToRgb(hexColor);
document.querySelector(':root').style.setProperty('--nevysha-font-color', `rgb(${rgbColor})`);
document.querySelector(':root').style.setProperty('--nevysha-font-color-subdued', getSubduedFontColor(hexColor));
}
export function applyBgGradiantColor(hexColor) {
COZY_NEST_CONFIG.bg_gradiant_color = hexColor;
const rgbColor = hexToRgb(hexColor);
document.querySelector(':root').style.setProperty('--nevysha-gradiant-1', `rgb(${rgbColor})`);
}
export function applyAccentColor(hexColor, colorFromLuminance) {
COZY_NEST_CONFIG.accent_color = hexColor;
const rgbColor = hexToRgb(hexColor);
document.querySelector(':root').style.setProperty('--ae-primary-color', `rgb(${rgbColor})`);
if (getLuminance(colorFromLuminance) > 0.5) {
@ -28,6 +34,169 @@ export function applyAccentColor(hexColor, colorFromLuminance) {
}
}
export const applyDisabledWavesAndGradiant = (disableWavesAndGradiant) => {
COZY_NEST_CONFIG.disable_waves_and_gradiant = disableWavesAndGradiant;
const $waves = $('.wave');
const $body = $('body');
if (disableWavesAndGradiant) {
$waves.css('animation', 'none');
$body.css('animation', 'none');
$body.css('background-position', '75% 75%')
}
else {
$waves.css('animation', '');
$body.css('animation', '');
$body.css('background-position', '')
}
}
export const applyAccentForGenerate = (checked, hexColorForAccent) => {
COZY_NEST_CONFIG.accent_generate_button = checked;
document.querySelectorAll('button[id$="_generate"]').forEach((btn) => {
if (checked) {
let txtColorAppending = "";
if (getLuminance(hexColorForAccent) > 0.5) {
txtColorAppending = "color: black !important";
}
btn.setAttribute("style", `background: var(--ae-primary-color) !important; ${txtColorAppending}`);
} else {
btn.setAttribute("style", '');
}
})
}
export const applyFontSize = (fontSize) => {
COZY_NEST_CONFIG.font_size = fontSize;
document.querySelector(':root').style.setProperty('--nevysha-text-md', `${fontSize}px`);
recalcOffsetFromMenuHeight()
}
export const setCardHeight = (cardHeight) => {
COZY_NEST_CONFIG.card_height = cardHeight;
document.querySelector(':root').style.setProperty('--extra-network-card-height', `${cardHeight}em`);
}
export const setCardWidth = (cardWidth) => {
COZY_NEST_CONFIG.card_width = cardWidth;
document.querySelector(':root').style.setProperty('--extra-network-card-width', `${cardWidth}em`);
}
export const applyMenuPosition = (position) => {
COZY_NEST_CONFIG.main_menu_position = position;
//top mode
if (position === "top" || position === "top_centered") {
document.querySelector(".nevysha.nevysha-tabnav").classList.add("menu-fix-top")
document.querySelector(".gradio-container.app").classList.add("menu-fix-top")
document.querySelector("#nevysha-btn-menu-wrapper")?.classList.add("menu-fix-top")
document.querySelector(':root').style.setProperty('--nevysha-margin-left', `0`);
document.querySelector(':root').style.setProperty('--menu-top-height', `25px`);
//centered or not
if (position === "top_centered") {
document.querySelector(".nevysha.nevysha-tabnav").classList.add("center-menu-items")
} else {
document.querySelector(".nevysha.nevysha-tabnav").classList.remove("center-menu-items")
}
}
//left mode
else {
document.querySelector(".nevysha.nevysha-tabnav").classList.remove("center-menu-items")
document.querySelector(".nevysha.nevysha-tabnav").classList.remove("menu-fix-top")
document.querySelector(".gradio-container.app").classList.remove("menu-fix-top")
document.querySelector("#nevysha-btn-menu-wrapper")?.classList.remove("menu-fix-top")
document.querySelector(':root').style.setProperty('--nevysha-margin-left', `175px`);
document.querySelector(':root').style.setProperty('--menu-top-height', `1px`);
}
recalcOffsetFromMenuHeight()
}
export const setQuicksettingPosition = (position) => {
COZY_NEST_CONFIG.quicksettings_position = position;
if (position === 'split') {
document.querySelector("#quicksettings_gap").classList.add("nevysha-quicksettings-gap")
document.querySelector("#quicksettings").classList.remove("centered-quicksettings")
}
else if (position === 'centered') {
document.querySelector("#quicksettings_gap").classList.remove("nevysha-quicksettings-gap")
document.querySelector("#quicksettings").classList.add("centered-quicksettings")
}
else {
document.querySelector("#quicksettings_gap").classList.remove("nevysha-quicksettings-gap")
document.querySelector("#quicksettings").classList.remove("centered-quicksettings")
}
}
export const setSfwSettings = (isSfwChecked) => {
COZY_NEST_CONFIG.sfw_mode = isSfwChecked;
if (isSfwChecked) {
document.querySelector('body').classList.add("nsfw");
}
else {
document.querySelector('body').classList.remove("nsfw");
}
}
export const recalcOffsetFromMenuHeight = () => {
let menuHeight = 0;
const tabs = document.getElementById('tabs');
const footer = document.querySelector('#footer #footer');
let footerHeight;
if (!footer) {
if (COZY_NEST_CONFIG.webui === WEBUI_SDNEXT)
footerHeight = 5;
else
footerHeight = 0;
}
else {
footerHeight = footer.offsetHeight;
}
if (COZY_NEST_CONFIG.main_menu_position !== 'left') {
const menu = document.querySelector('.tab-nav.nevysha-tabnav')
menuHeight = menu.offsetHeight + 2;
document.querySelector(':root').style.setProperty('--menu-top-height', `${menuHeight}px`);
const $app = $('.gradio-container.app');
$app.attr('style', `${$app.attr('style')} padding-top: ${menuHeight}px !important;`);
const rect = tabs.getBoundingClientRect();
const tabsTop = rect.top;
document.querySelector(':root').style.setProperty('--main-container-height', `${window.innerHeight - (tabsTop + footerHeight)}px`);
window.troubleshootSize = {
menuHeight,
footerHeight: footerHeight,
tabsTop,
WindowInnerHeight: window.innerHeight,
bodyHeight: window.innerHeight - (tabsTop + footerHeight),
'main-container-height': `${window.innerHeight - (tabsTop + footerHeight)}px`,
}
}
else {
document.querySelector(':root').style.setProperty('--menu-top-height', `1px`);
const $app = $('.gradio-container.app');
$app.attr('style', `${$app.attr('style')} padding-top: ${menuHeight}px !important;`);
const rect = tabs.getBoundingClientRect();
const tabsTop = rect.top;
document.querySelector(':root').style.setProperty('--main-container-height', `${window.innerHeight - (tabsTop + footerHeight)}px`);
window.troubleshootSize = {
menuHeight,
footerHeight: footerHeight,
tabsTop,
WindowInnerHeight: window.innerHeight,
bodyHeight: window.innerHeight - (tabsTop + footerHeight),
'main-container-height': `${window.innerHeight - (tabsTop + footerHeight)}px`,
}
}
}
export const wrapDataGenerationInfo = ({prefix}) => {
// Get the generation info container
const previewBlocks = document.querySelectorAll(`#tab_${prefix} div#${prefix}_results > *:not(#${prefix}_results)`);

View File

@ -8,13 +8,32 @@
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@chakra-ui/react": "^2.7.0",
"@fontsource-variable/caveat": "^5.0.0",
"ace": "^1.3.0",
"ace-builds": "^1.22.0",
"animate.css": "^4.1.1",
"jquery": "^3.7.0",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-select": "^5.7.3",
"react-spinners": "^0.13.8",
"react-use-websocket": "^3.0.0",
"showdown": "^2.1.0"
},
"devDependencies": {
"@babel/plugin-syntax-import-assertions": "^7.20.0",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.0.4",
"vite": "^4.3.2"
}
}

View File

@ -0,0 +1,111 @@
.App {
/*TODO calc this*/
width: max(800px, 50vw);
height: fit-content;
min-height: 150px;
z-index: 9998;
border: 1px solid var(--ae-input-border-color);
background-color: var(--block-background-fill);
font-weight: var(--body-text-weight);
font-size: var(--body-text-size);
color: var(--button-secondary-text-color);
}
.nevysha-btn-menu {
fill: var(--ae-primary-color);
width: 20px;
}
.container {
border-top: none;
border-bottom-right-radius: var(--container-radius);
border-bottom-left-radius: var(--container-radius);
padding: var(--block-padding);
}
.btn-settings {
cursor: pointer;
position: relative;
width: 60px;
height: 35px;
display: flex;
place-content: center;
flex-wrap: wrap;
background: var(--ae-input-bg-color);
border: 1px solid var(--ae-input-border-color);
border-radius: 0;
color: var(--body-text-color);
font-size: var(--body-text-size);
}
.btn-settings:hover {
background: var(--ae-input-bg-color);
border: 1px solid var(--ae-input-border-color);
}
.btn-settings:active {
outline: none;
}
.btn-toolbar {
padding: 10px 0 20px;
}
.title {
width: 100%;
text-align: center;
position: absolute;
top: 10px;
}
.title > h2 {
font-size: 1.8em;
color: var(--ae-primary-color);
font-family: "Caveat Variable", sans-serif;
margin: 0;
}
.title > .subtitle {
font-size: 10px;
font-style: italic;
}
.picker {
position: relative;
}
.swatch {
width: 28px;
height: 28px;
border: 1px solid var(--ae-input-border-color);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.popoverWrap {
position: fixed;
z-index: 29;
width: 100%;
height: 100%;
top: 0;
left: 0;
/*background-color: #1a1a1a;*/
}
.popover {
position: fixed;
border-radius: 9px;
z-index: 30;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.header {
}
.OutputFolderSelector {
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 5px;
}
.nevysha-reporting a {
margin: 0 4px;
}

View File

@ -0,0 +1,405 @@
import React, {useEffect, useRef, useState} from "react";
import './App.css'
import {svg_magic_wand} from "../main/svg.js";
import {Header} from "./Header.jsx";
import {Column, Row, RowFullWidth} from "../main/Utils.jsx";
import {
Radio,
RadioGroup,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
Checkbox,
Stack,
Input,
FormControl,
FormLabel,
NumberInput,
NumberInputStepper,
NumberIncrementStepper,
NumberInputField,
NumberDecrementStepper,
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogOverlay,
useDisclosure,
} from '@chakra-ui/react'
import {PopoverColorPicker} from "./PopoverColorPicker.jsx";
import {OuputFolderSelector} from "./OuputFolderSelector.jsx";
import {
applyAccentColor,
applyBgGradiantColor,
applyFontColor,
applyWavesColor,
applyDisabledWavesAndGradiant,
applyAccentForGenerate,
applyFontSize,
setCardHeight,
setCardWidth,
applyMenuPosition, setQuicksettingPosition, setSfwSettings, recalcOffsetFromMenuHeight
} from "../main/tweaks/various-tweaks.js";
import {getTheme} from "../main/cozy-utils.js";
import {WEBUI_A1111, WEBUI_SDNEXT} from "../main/Constants.js";
import {saveCozyNestConfig} from "../main/nevysha-cozy-nest.js";
import {ButtonWithConfirmDialog} from "../chakra/ButtonWithConfirmDialog.jsx";
function DialogWrapper({children, isVisible}) {
const { isOpen, onOpen, onClose } = useDisclosure({isOpen: isVisible})
const cancelRef = useRef()
useEffect(() => {
if (isVisible) {
onOpen()
}
else {
onClose()
}
}, [isVisible])
return (
<AlertDialog
motionPreset='scale'
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogBody>
{children}
</AlertDialogBody>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
}
const nevyshaScrollbar = {
'&::-webkit-scrollbar': {
width: '5px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'transparent'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'var(--ae-primary-color)',
borderRadius: '20px',
},
}
export function App() {
const [isVisible, setIsVisible] = useState(false)
const [config, setConfig] = useState(COZY_NEST_CONFIG)
function applySettings() {
applyWavesColor(config.waves_color);
applyFontColor(
getTheme() === "dark" ?
config.font_color :
config.font_color_light
)
applyBgGradiantColor(config.bg_gradiant_color);
applyDisabledWavesAndGradiant(config.disable_waves_and_gradiant);
applyAccentColor(config.accent_color, config.accent_color);
applyAccentForGenerate(config.accent_generate_button, config.accent_color);
applyFontSize(config.font_size)
setCardHeight(config.card_height)
setCardWidth(config.card_width)
applyMenuPosition(config.main_menu_position)
setQuicksettingPosition(config.quicksettings_position)
setSfwSettings(config.sfw_mode)
recalcOffsetFromMenuHeight()
}
useEffect(() => {
applySettings();
}, [config])
useEffect(() => {
setConfig(COZY_NEST_CONFIG)
}, [COZY_NEST_CONFIG])
useEffect(() => {
applySettings();
}, [])
const toggle = () => {
CozyLogger.debug('toggle')
setIsVisible(!isVisible)
}
const updateConfig = (e, what) => {
const newConfig = {...config}
if (e.target)
newConfig[what] = e.target.value
else
newConfig[what] = e
setConfig(newConfig)
}
const saveConfig = () => {
(async () => await saveCozyNestConfig(config))()
}
const resetConfig = () => {
(async () => {
// call to @app.delete("/cozy-nest/config")
await fetch(`/cozy-nest/config`, {
method: 'DELETE',
})
})();
}
const reloadUi = () => {
if (config.webui === WEBUI_A1111) {
document.querySelector('#settings_restart_gradio').click();
return;
}
else if (config.webui === WEBUI_SDNEXT) {
document.querySelector('#restart_submit').click();
return;
}
(async () => {
// call to @app.get("/cozy-nest/reloadui")
await fetch(`/cozy-nest/reloadui`)
//reload the page
window.location.reload()
})();
}
return (
<div style={{display: 'flex'}}>
<button className="nevysha-btn-menu"
id="nevyui_sh_options"
title="Nevysha Cozy Nest Settings"
dangerouslySetInnerHTML={{__html:svg_magic_wand}}
onClick={toggle}
/>
{ isVisible &&
<DialogWrapper isVisible={isVisible}>
<div className="App nevysha">
<Header onClickClose={() => setIsVisible(false)}/>
<div className="container">
<Tabs variant='nevysha'>
<TabList style={{backgroundColor: 'var(--tab-nav-background-color)'}}>
<Tab>Main Settings</Tab>
<Tab>Image Browser Settings</Tab>
<Tab>Others</Tab>
</TabList>
<TabPanels>
<TabPanel css={nevyshaScrollbar}>
<RowFullWidth>
<Checkbox
isChecked={config.error_popup}
onChange={(e) => setConfig({...config, error_popup: e.target.checked})}
>Display information dialog on Cozy Nest error</Checkbox>
<Checkbox
isChecked={config.disable_waves_and_gradiant}
onChange={(e) => setConfig({...config, disable_waves_and_gradiant: e.target.checked})}
>Disable waves and gradiant background animations</Checkbox>
</RowFullWidth>
<Column>
<label>Main menu position</label>
<RadioGroup
value={config.main_menu_position}
onChange={(value) => setConfig({...config, main_menu_position: value})}
>
<Stack direction='row'>
<Radio value='left'>left</Radio>
<Radio value='top'>top</Radio>
<Radio value='top_centered'>top centered</Radio>
</Stack>
</RadioGroup>
</Column>
<Column>
<label>Quicksettings position</label>
<RadioGroup
value={config.quicksettings_position}
onChange={(value) => setConfig({...config, quicksettings_position: value})}
>
<Stack direction='row'>
<Radio value='left'>left</Radio>
<Radio value='split'>split</Radio>
<Radio value='centered'>centered</Radio>
</Stack>
</RadioGroup>
</Column>
<Row>
<Checkbox
isChecked={config.accent_generate_button}
onChange={(e) => setConfig({...config, accent_generate_button: e.target.checked})}
>Accent Generate Button</Checkbox>
</Row>
<RowFullWidth>
<FormControl>
<FormLabel>Font size</FormLabel>
<NumberInput defaultValue={12} min={10} max={18}>
<NumberInputField/>
<NumberInputStepper>
<NumberIncrementStepper/>
<NumberDecrementStepper/>
</NumberInputStepper>
</NumberInput>
</FormControl>
<FormControl>
<FormLabel>Extra network card height</FormLabel>
<NumberInput
value={config.card_height}
onChange={(e) => updateConfig(e, 'card_height')}
min={5} max={20}
>
<NumberInputField/>
<NumberInputStepper>
<NumberIncrementStepper/>
<NumberDecrementStepper/>
</NumberInputStepper>
</NumberInput>
</FormControl>
<FormControl>
<FormLabel>Extra network card width</FormLabel>
<NumberInput
value={config.card_width}
onChange={(e) => updateConfig(e, 'card_width')}
min={5} max={20}
>
<NumberInputField/>
<NumberInputStepper>
<NumberIncrementStepper/>
<NumberDecrementStepper/>
</NumberInputStepper>
</NumberInput>
</FormControl>
</RowFullWidth>
<RowFullWidth>
<PopoverColorPicker
label="Font Color"
color={config.font_color}
onChange={(e) => updateConfig(e, 'font_color')} />
<PopoverColorPicker
label="Font Color"
color={config.font_color_light}
onChange={(e) => updateConfig(e, 'font_color_light')} />
<PopoverColorPicker
label="Waves Color"
color={config.waves_color}
onChange={(e) => updateConfig(e, 'waves_color')} />
<PopoverColorPicker
label="Background gradiant Color"
color={config.bg_gradiant_color}
onChange={(e) => updateConfig(e, 'bg_gradiant_color')} />
<PopoverColorPicker
label="Accent Color"
color={config.accent_color}
onChange={(e) => updateConfig(e, 'accent_color')} />
</RowFullWidth>
<RowFullWidth>
<Checkbox
isChecked={config.sfw_mode}
onChange={(e) => setConfig({...config, sfw_mode: e.target.checked})}
>SFW mode 👀 (blur all images)</Checkbox>
</RowFullWidth>
</TabPanel>
<TabPanel css={nevyshaScrollbar}>
<RowFullWidth>
<Checkbox
isChecked={config.disable_image_browser}
onChange={(e) => setConfig({...config, disable_image_browser: e.target.checked})}
>Disable image browser (Reload UI required)</Checkbox>
</RowFullWidth>
<RowFullWidth>
<FormControl style={{width: "30%"}}>
<FormLabel>Socket port for image browser</FormLabel>
<Input
placeholder='3333'
value={config.server_default_port}
onChange={(e) => updateConfig(e, 'server_default_port')}
/>
</FormControl>
<Checkbox
>Auto search port</Checkbox>
<Checkbox
visibility={config.webui === WEBUI_SDNEXT ? 'hidden' : 'visible'}
isChecked={config.fetch_output_folder_from_a1111_settings}
onChange={(e) => setConfig({...config, fetch_output_folder_from_a1111_settings: e.target.checked})}
>Fetch output folder from a1111 settings (Reload needed to enable)</Checkbox>
</RowFullWidth>
<Column>
<FormLabel>Archive path</FormLabel>
<Input
placeholder='C:/stable-difusion/...'
value={config.archive_path}
onChange={(e) => updateConfig(e, 'archive_path')}
/>
</Column>
<Column>
<FormLabel>Output path</FormLabel>
<OuputFolderSelector config={config} setConfig={setConfig}/>
</Column>
</TabPanel>
<TabPanel css={nevyshaScrollbar}>
<p>Those settings are heavy on DOM modification and might conflict with some others extensions</p>
<p>Reload UI needed to apply</p>
<Column>
<Checkbox
isChecked={config.enable_clear_button}
onChange={(e) => setConfig({...config, enable_clear_button: e.target.checked})}
>Enable clear gallery button in txt2img and img2img tabs</Checkbox>
<Checkbox
isChecked={config.enable_extra_network_tweaks}
onChange={(e) => setConfig({...config, enable_extra_network_tweaks: e.target.checked})}
>Enable extra network tweaks</Checkbox>
<Checkbox
isChecked={config.enable_cozy_prompt}
onChange={(e) => setConfig({...config, enable_cozy_prompt: e.target.checked})}
>Enable Cozy Prompt</Checkbox>
</Column>
</TabPanel>
</TabPanels>
</Tabs>
<RowFullWidth className="btn-toolbar" style={{gap: '25px', padding: '15px'}}>
<button
className="btn-settings"
style={{width: '100%'}}
onClick={saveConfig}
>Save</button>
<ButtonWithConfirmDialog
message="Are you sure you want to reset all settings ? This will trigger a UI Reload"
confirmLabel="Reset"
cancelLabel="Cancel"
onConfirm={resetConfig}
/>
<button
className="btn-settings"
style={{width: '100%'}}
onClick={reloadUi}
>Reload UI</button>
</RowFullWidth>
<div>Made by Nevysha with luv</div>
</div>
</div>
</DialogWrapper>
}
</div>
)
}

View File

@ -0,0 +1,34 @@
import React from "react";
import {Row} from "../main/Utils.jsx";
import './App.css'
export function Header (props) {
const gatherInfoAndShowDialog = () => {
window.gatherInfoAndShowDialog()
props.onClickClose()
}
return (
<div className="nevysha header">
<div className="nevysha title">
<h2>Nevysha's Cozy Nest</h2>
<span className="subtitle">Find your cozy spot on Auto1111's webui</span>
</div>
<Row>
<div className="btn-settings" onClick={props.onClickClose}>Close</div>
</Row>
<div className="container">
<div className="nevysha settings-nevyui-top"><p className="nevysha-reporting">Found a bug or want to ask for
a feature ? Please <a onClick={gatherInfoAndShowDialog} target="_blank">click
here to gather relevant info</a> then use <a href="https://www.reddit.com/r/NevyshaCozyNest/"
target="_blank">this subreddit</a> or <a
href="https://github.com/Nevysha/Cozy-Nest" target="_blank">github</a>. You can also join this <a
href="https://discord.gg/yppzDXjT7S" target="_blank">discord server</a> to discuss about Cozy Nest
</p><p className="nevysha-emphasis">WARNING : Some visual settings are immediately applied but will not be
saved until you click "Save"</p></div>
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
import React, {useEffect, useState} from "react";
import {Input, InputGroup, InputRightElement} from "@chakra-ui/react";
export function OuputFolderSelector({config, setConfig}) {
const [outputFolder, setOutputFolder] = useState(config.cnib_output_folder)
const [newOutputFolder, setNewOutputFolder] = useState('')
useEffect(() => {
setOutputFolder(config.cnib_output_folder)
}, [config]);
const addNewOutputFolder = () => {
if (newOutputFolder === '') {
return
}
const newOutputFolderArray = [...outputFolder]
newOutputFolderArray.push(newOutputFolder)
setOutputFolder(newOutputFolderArray)
setConfig({...config, cnib_output_folder: newOutputFolderArray})
setNewOutputFolder('')
}
return (
<>
<div className="OutputFolderSelector">
{outputFolder.map((folder, index) => {
return (
<InputGroup key={index}>
<Input
placeholder="C:/stable-difusion/..."
value={folder}
onChange={(e) => {
const newOutputFolder = [...outputFolder]
newOutputFolder[index] = e.target.value
setOutputFolder(newOutputFolder)
setConfig({...config, cnib_output_folder: newOutputFolder})
}}
/>
<InputRightElement width='4.5rem'>
<button
className="btn-settings"
onClick={() => {
const newOutputFolder = [...outputFolder]
newOutputFolder.splice(index, 1)
setOutputFolder(newOutputFolder)
setConfig({...config, cnib_output_folder: newOutputFolder})
}}
>Delete</button>
</InputRightElement>
</InputGroup>
)
})}
<InputGroup>
<Input
placeholder="Add a new folder..."
value={newOutputFolder}
onChange={(e) => {
setNewOutputFolder(e.target.value)
}}
/>
<InputRightElement width='4.5rem'>
<button className="btn-settings" onClick={addNewOutputFolder}>Add</button>
</InputRightElement>
</InputGroup>
</div>
</>
);
}

View File

@ -0,0 +1,29 @@
import React, {useCallback, useEffect, useRef, useState} from "react";
import { HexColorPicker } from "react-colorful";
export const PopoverColorPicker = ({ color, onChange, label }) => {
const popover = useRef();
const [isOpen, toggle] = useState(false);
return (
<div>
<label>{label}</label>
<div className="picker">
<div
className="swatch"
style={{ backgroundColor: color }}
onClick={() => toggle(true)}
/>
{isOpen && (
<>
<div className="popoverWrap" onClick={() => toggle(false)} />
<div className="popover" ref={popover}>
<HexColorPicker color={color} onChange={onChange} />
</div>
</>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,29 @@
import React from "react";
import ReactDOM from 'react-dom/client'
import {App} from "./App.jsx";
import {ChakraProvider} from '@chakra-ui/react'
import {theme} from "../chakra/chakra-theme.ts";
const containerId = 'cozy_nest_options';
export default function startCozyNestSettings() {
if (!document.getElementById('nevysha-btn-menu-wrapper')) {
setTimeout(() => startCozyNestSettings(), 200)
return
}
//add a div to hold settings
const settingsDiv = document.createElement("div");
settingsDiv.id = containerId;
settingsDiv.style = 'display: flex;'
// insert settingsDiv before the first child of '#nevysha-btn-menu-wrapper'
document.getElementById('nevysha-btn-menu-wrapper').insertBefore(settingsDiv, document.getElementById('nevysha-btn-menu-wrapper').firstChild);
ReactDOM.createRoot(document.getElementById(containerId)).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<App />
</ChakraProvider >
</React.StrictMode>,
)
}

View File

@ -0,0 +1,35 @@
import { useEffect } from "react";
// Improved version of https://usehooks.com/useOnClickOutside/
const useClickOutside = (ref, handler) => {
useEffect(() => {
let startedInside = false;
let startedWhenMounted = false;
const listener = (event) => {
// Do nothing if `mousedown` or `touchstart` started inside ref element
if (startedInside || !startedWhenMounted) return;
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) return;
handler(event);
};
const validateEventStart = (event) => {
startedWhenMounted = ref.current;
startedInside = ref.current && ref.current.contains(event.target);
};
document.addEventListener("mousedown", validateEventStart);
document.addEventListener("touchstart", validateEventStart);
document.addEventListener("click", listener);
return () => {
document.removeEventListener("mousedown", validateEventStart);
document.removeEventListener("touchstart", validateEventStart);
document.removeEventListener("click", listener);
};
}, [ref, handler]);
};
export default useClickOutside;

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext", "ES6"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true
},
"include": ["main", "image-browser"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext", "ES6"],
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,66 +0,0 @@
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
{
name: "configure-response-headers",
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-none");
res.setHeader("Cross-Origin-Opener-Policy", "unsafe-non");
next();
});
},
},
{
name: 'route-default-to-index',
configureServer: (server) => {
server.middlewares.use(async (_req, res, next) => {
if (_req.originalUrl === '/cozy-nest-client'
|| _req.originalUrl === '/cozy-nest-client?__theme=dark'
|| _req.originalUrl === '/cozy-nest-client?__theme=light') {
let updatedResponse =await (await fetch('http://127.0.0.1:7860/')).text()
// replace </body> with </body><script type="module" src="/main.js"></script>
updatedResponse = updatedResponse.replace('</body>', '</body><script type="module" src="/cozy-nest-client/main.js"></script>')
// Set the modified response
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('charset', 'utf-8');
res.end(updatedResponse);
return;
}
// Continue to the next middleware
next();
});
}
}
],
build: {
outDir: '../client',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
},
server: {
host: '127.0.0.1',
port: 5173,
proxy: {
'/queue/join': {
target: 'ws://127.0.0.1:7860',
ws: true,
},
//route everything except /cozy-nest-client/ to localhost:7860
'^(?!.*cozy-nest-client).*$': 'http://127.0.0.1:7860',
}
},
base: 'cozy-nest-client'
})

View File

@ -0,0 +1,86 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@image-browser': './image-browser/',
'@settings': './settings/',
'@main': './main/'
}
},
plugins: [
react({
babel: {
plugins: ['@babel/plugin-syntax-import-assertions'],
},
}),
{
name: "configure-response-headers",
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-none");
res.setHeader("Cross-Origin-Opener-Policy", "unsafe-non");
next();
});
},
},
{
name: 'route-default-to-index',
configureServer: (server) => {
server.middlewares.use(
async (_req, res, next): Promise<void> => {
if (_req.originalUrl === '/cozy-nest-client'
|| _req.originalUrl === '/cozy-nest-client?__theme=dark'
|| _req.originalUrl === '/cozy-nest-client?__theme=light') {
let updatedResponse =await (await fetch('http://127.0.0.1:7860/')).text()
const toAdd = `
<script type="module" src="/cozy-nest-client/main/_dev.js"></script>
<script type="module" src="/cozy-nest-client/main.jsx"></script>
`
// replace </body> with </body><script type="module" src="/main.js"></script>
updatedResponse = updatedResponse.replace('</body>', `</body>${toAdd}`)
// Set the modified response
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('charset', 'utf-8');
res.end(updatedResponse);
return;
}
// Continue to the next middleware
next();
});
}
}
],
build: {
outDir: '../client',
rollupOptions: {
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
}
},
server: {
host: '127.0.0.1',
port: 5173,
proxy: {
'/queue/join': {
target: 'ws://127.0.0.1:7860',
ws: true,
},
'http://127.0.0.1:5173/theme-cozy-json.js': 'http://127.0.0.1:5173/cozy-nest-client/image-browser/src/editor/theme-cozy-json.js',
//route everything except /cozy-nest-client/ to localhost:7860
'^(?!.*cozy-nest-client).*$': 'http://127.0.0.1:7860',
}
},
base: 'cozy-nest-client'
})

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

0
data/.keepme Normal file
View File

View File

@ -12,25 +12,7 @@ import modules.images
import websockets
from websockets.server import serve
def get_exif(path):
allExif = {}
try:
image = Image.open(path)
# info = image.info
(_, allExif, allExif_html) = modules.extras.run_pnginfo(image)
image.close()
except Exception as e:
print(f"CozyNestSocket: WARNING cannot get exif data for image {path}")
pass
img = {
'path': path,
'metadata': {
'date': os.path.getmtime(path),
'exif': allExif,
}
}
return img
from scripts import tools
async def start_server(images_folders, server_port, stopper):
@ -73,24 +55,7 @@ async def start_server(images_folders, server_port, stopper):
async def process(data):
what = data['what']
if what == 'images':
# scrape the images folder recursively
images = []
for images_folder in images_folders:
for root, dirs, files in os.walk(images_folder):
for file in files:
if file.endswith(".png"):
# get exif data
img = get_exif(os.path.join(root, file))
images.append(img)
# sort the images by date (newest first) metadata.date
images.sort(key=lambda x: x['metadata']['date'], reverse=True)
# send the images to the client
data = {
'what': 'images',
'images': images
}
data = tools.scrap_image_folders(images_folders)
return json.dumps(data)
if what == 'image_saved':
@ -100,6 +65,13 @@ async def start_server(images_folders, server_port, stopper):
'data': 'None'
})
if what == 'index_built':
await on_index_built(data['data'])
return json.dumps({
'what': 'success',
'data': 'None'
})
else:
print(f"CozyNestSocket: Unknown data: {data}")
return json.dumps({
@ -116,6 +88,14 @@ async def start_server(images_folders, server_port, stopper):
for websocket in CLIENTS_COPY.copy():
await websocket_send('dispatch_on_image_saved', data, websocket)
async def on_index_built(data):
CLIENTS_COPY = CLIENTS.copy()
CLIENTS.clear()
for websocket in CLIENTS_COPY.copy():
await websocket_send('dispatch_on_index_built', data, websocket)
async def websocket_send(what, data, websocket):
try:
await websocket.send(json.dumps({

View File

@ -9,13 +9,16 @@ import threading
import gradio as gr
import modules
from PIL import Image
from PIL.PngImagePlugin import PngInfo
from typing import Any
from fastapi import FastAPI, Response, Request
from fastapi.staticfiles import StaticFiles
import websockets
from modules import script_callbacks, shared, call_queue, scripts
from scripts.cozynest_image_browser import start_server, get_exif
from scripts import tools
from scripts.cozynest_image_browser import start_server
def rgb_to_hex(r, g, b):
@ -55,6 +58,7 @@ def gradio_save_settings(main_menu_position,
auto_search_port,
auto_start_server,
fetch_output_folder_from_a1111_settings,
archive_path,
sfw_mode,
enable_clear_button,
enable_extra_network_tweaks,
@ -81,6 +85,7 @@ def gradio_save_settings(main_menu_position,
'sfw_mode': sfw_mode,
'enable_clear_button': enable_clear_button,
'enable_extra_network_tweaks': enable_extra_network_tweaks,
'archive_path': archive_path,
}
current_config = get_dict_from_config()
@ -131,11 +136,13 @@ def get_default_settings():
'server_default_port': 3333,
'auto_search_port': True,
'auto_start_server': True,
'fetch_output_folder_from_a1111_settings': True,
'fetch_output_folder_from_a1111_settings': False,
'cnib_output_folder': [],
'archive_path': '',
'sfw_mode': False,
'enable_clear_button': True,
'enable_extra_network_tweaks': True,
'enable_cozy_prompt': True,
'webui': 'unknown'
}
@ -240,186 +247,6 @@ def start_server_in_dedicated_process(_images_folders, server_port):
script_callbacks.on_before_reload(stop_server)
def gradio_img_browser_tab(config):
with gr.Column(elem_id="img_browser_main_block"):
# disable_image_browser
disable_image_browser = gr.Checkbox(value=config.get('disable_image_browser'),
label="Disable image browser (Reload UI required)",
elem_id="setting_nevyui_disableImageBrowser", interactive=True)
with gr.Row():
server_default_port = gr.Number(value=config.get('server_default_port'),
label="Socket port for image browser", interactive=True, precision=0)
auto_search_port = gr.Checkbox(value=True, label="Auto search port",
elem_id="setting_nevyui_autoSearchPort",
interactive=True)
auto_start_server = gr.Checkbox(value=config.get('auto_start_server'), label="Auto start server",
elem_id="setting_nevyui_autoStartServer",
interactive=True, visible=False)
fetch_output_folder_from_a1111_settings = gr.Checkbox(
value=config.get('fetch_output_folder_from_a1111_settings'),
label="Fetch output folder from a1111 settings (Reload needed to enable)",
elem_id="setting_nevyui_fetchOutputFolderFromA1111Settings",
interactive=True)
# Add a text block to display each folder from output_folder_array()
with gr.Blocks(elem_id="img_browser_folders_block"):
# TODO refactor to remove this as it's no longer managed through gradio
gr.Textbox(value=json.dumps(config.get('cnib_output_folder')), label="Output folder",
elem_id="cnib_output_folder", interactive=True, visible=False)
return [
disable_image_browser,
server_default_port,
auto_search_port,
auto_start_server,
fetch_output_folder_from_a1111_settings]
def gradio_main_tab(config):
with gr.Column(elem_id="nevyui-ui-block"):
with gr.Row():
# error popup checkbox
error_popup = gr.Checkbox(value=config.get('error_popup'),
label="Display information dialog on Cozy Nest error",
elem_id="setting_nevyui_errorPopup", interactive=True)
# disable waves and gradiant bg
disable_waves_and_gradiant = gr.Checkbox(value=config.get('disable_waves_and_gradiant'),
label="Disable waves and gradiant background animations",
elem_id="setting_nevyui_disableWavesAndGradiant", interactive=True)
# main menu
main_menu_position = gr.Radio(value=config.get('main_menu_position'), label="Main menu position",
choices=['left', 'top', 'top_centered'],
elem_id="setting_nevyui_menuPosition", interactive=True)
quicksettings_position = gr.Radio(value=config.get('quicksettings_position'),
label="Quicksettings position",
choices=['left', 'split', 'centered'],
elem_id="setting_nevyui_quicksettingsPosition", interactive=True)
accent_generate_button = gr.Checkbox(value=config.get('accent_generate_button'),
label="Accent Generate Button",
elem_id="setting_nevyui_accentGenerateButton", interactive=True)
with gr.Row():
font_size = gr.Slider(value=config.get('font_size'), label="Font size", minimum=10, maximum=18, step=1,
elem_id="setting_nevyui_fontSize", interactive=True)
card_height = gr.Slider(value=config.get('card_height'), label="Extra network card height", minimum=5,
maximum=20, step=1, elem_id="setting_nevyui_cardHeight", interactive=True)
card_width = gr.Slider(value=config.get('card_width'), label="Extra network card width", minimum=5,
maximum=20, step=1, elem_id="setting_nevyui_cardWidth", interactive=True)
with gr.Row():
font_color = gr.ColorPicker(value=config.get('font_color'), label="Font color",
elem_id="setting_nevyui_fontColor", interactive=True, visible=False)
font_color_light = gr.ColorPicker(value=config.get('font_color_light'), label="Font color",
elem_id="setting_nevyui_fontColorLight", interactive=True, visible=False)
waves_color = gr.ColorPicker(value=config.get('waves_color'), label="Waves color",
elem_id="setting_nevyui_waveColor", interactive=True)
bg_gradiant_color = gr.ColorPicker(value=config.get('bg_gradiant_color'),
label="Background gradiant color",
elem_id="setting_nevyui_bgGradiantColor", interactive=True)
accent_color = gr.ColorPicker(value=config.get('accent_color'), label="Accent color",
elem_id="setting_nevyui_accentColor", interactive=True)
sfw_mode = gr.Checkbox(value=config.get('sfw_mode'),
label="SFW mode 👀 (blur all images)",
elem_id="setting_nevyui_sfwMode", interactive=True)
return [
accent_color,
accent_generate_button,
bg_gradiant_color,
card_height,
card_width,
disable_waves_and_gradiant,
error_popup,
font_size,
main_menu_position,
quicksettings_position,
font_color,
font_color_light,
waves_color,
sfw_mode,
]
def ui_action_btn(accent_color, accent_generate_button, bg_gradiant_color, card_height, card_width,
disable_waves_and_gradiant, error_popup, font_size, main_menu_position,
quicksettings_position, font_color, font_color_light, waves_color, disable_image_browser,
server_default_port,
auto_search_port,
auto_start_server,
fetch_output_folder_from_a1111_settings, sfw_mode, enable_clear_button, enable_extra_network_tweaks):
with gr.Row():
btn_save = gr.Button(value="Save", elem_id="nevyui_sh_options_submit",
elem_classes="nevyui_apply_settings")
btn_save.click(gradio_save_settings, inputs=[
main_menu_position,
quicksettings_position,
accent_generate_button,
font_size,
font_color,
font_color_light,
waves_color,
bg_gradiant_color,
accent_color,
card_height,
card_width,
error_popup,
disable_image_browser,
disable_waves_and_gradiant,
server_default_port,
auto_search_port,
auto_start_server,
fetch_output_folder_from_a1111_settings,
sfw_mode,
enable_clear_button,
enable_extra_network_tweaks,
], outputs=[])
btn_reset = gr.Button(value="Reset default (Reload UI needed to apply)",
elem_id="nevyui_sh_options_reset", elem_classes="nevyui_apply_settings")
# restore default settings
btn_reset.click(reset_settings)
btn_reload = gr.Button(value="Reload UI", elem_id="nevyui_sh_options_reset",
elem_classes="nevyui_apply_settings")
# reload the page
btn_reload.click(
fn=request_restart,
_js='restart_reload',
inputs=[],
outputs=[], )
# start socket server
btn_start = gr.Button(value="Start Socket Server", elem_id="nevyui_sh_options_start_socket",
elem_classes="nevyui_apply_settings")
btn_start.click(
fn=serv_img_browser_socket,
inputs=[],
outputs=[], )
with gr.Row(elem_id='nevysha-saved-feedback-wrapper'):
gr.HTML(
value="<div id='nevysha-saved-feedback' class='nevysha nevysha-feedback' style='display:none;'>Saved !</div>")
gr.HTML(
value="<div id='nevysha-reset-feedback' class='nevysha nevysha-feedback' style='display:none;'>Reset !</div>")
gr.HTML(
value="<div id='nevysha-dummy-feedback' class='nevysha nevysha-feedback' style='display:none;' />")
# add button to trigger git pull
btn_update = gr.Button(value="Update", elem_id="nevyui_sh_options_update", visible=False, )
btn_update.click(
fn=update,
inputs=[],
outputs=[], )
def gradio_hidden_field(server_port):
# text with port number
gr.Textbox(elem_id='cnib_socket_server_port', value=f"{server_port}", label="Server port READONLY",
@ -447,36 +274,11 @@ def gradio_hidden_field(server_port):
))
def prune_ui_settings(**kwargs):
# load file ui-config.json located in working directory
ui_config_path = shared.cmd_opts.ui_config_file
if ui_config_path is None:
ui_config_path = os.path.join(shared.get_app_dir(), 'ui-config.json')
if os.path.exists(ui_config_path):
with open(ui_config_path, 'r') as f:
ui_config = json.load(f)
# remove keys that contains "nevyui" prefix
pruned = False
for key in list(ui_config.keys()):
if key.startswith('nevyui'):
pruned = True
del ui_config[key]
if pruned:
print('CozyNest: Pruned ui-config.json')
# save the file
with open(ui_config_path, 'w') as f:
json.dump(ui_config, f, indent=4)
script_callbacks.on_before_reload(prune_ui_settings)
script_callbacks.on_app_started(lambda a, b: prune_ui_settings)
prune_ui_settings()
_server_port = None
def on_ui_tabs():
prune_ui_settings()
global _server_port
# shared options
config = get_dict_from_config()
# merge default settings with user settings
@ -519,25 +321,10 @@ def on_ui_tabs():
config.get('auto_search_port'),
config.get('cnib_output_folder')
)
_server_port = server_port
else:
print("CozyNest: Image browser is disabled. To enable it, go to the CozyNest settings.")
async def send_to_socket(data):
async with websockets.connect(f'ws://localhost:{server_port}') as websocket:
try:
while True:
# Send data to the server
data = json.dumps(data).encode('utf-8')
await websocket.send(data)
# Receive response from the server
await websocket.recv()
websocket.close()
break
except websockets.exceptions.ConnectionClosed:
pass
def on_image_saved(gen_params: script_callbacks.ImageSaveParams):
base_dir = scripts.basedir()
@ -551,10 +338,13 @@ def on_ui_tabs():
if not any([path.startswith(folder) for folder in images_folders]):
return
data = tools.get_exif(path)
tools.new_image(data)
asyncio.run(send_to_socket({
'what': 'image_saved',
'data': get_exif(path),
}))
'data': data,
}, server_port))
if not disable_image_browser_value:
script_callbacks.on_image_saved(on_image_saved)
@ -567,79 +357,34 @@ def on_ui_tabs():
" then use <a href='https://www.reddit.com/r/NevyshaCozyNest/'>this subreddit</a>"
" or <a href='https://github.com/Nevysha/Cozy-Nest'>github</a>. "
"You can also join this <a href='https://discord.gg/yppzDXjT7S'>discord server</a> to discuss about Cozy Nest</p>"
"<p class='nevysha-emphasis'>WARNING : Some visual settings are immediately applied but will not be saved until you click \"Save\"</p></div>")
with gr.Tabs(id="cozy_nest_settings_tabs", elem_id="cozy_nest_settings_tabs"):
with gr.TabItem(label="Main Settings", elem_id="cozy_nest_settings_tab"):
[
accent_color,
accent_generate_button,
bg_gradiant_color,
card_height,
card_width,
disable_waves_and_gradiant,
error_popup,
font_size,
main_menu_position,
quicksettings_position,
font_color,
font_color_light,
waves_color,
sfw_mode
] = gradio_main_tab(config)
with gr.TabItem(label="Image Browser Settings", elem_id="cozy_nest_img_browser_settings_tab"):
[
disable_image_browser,
server_default_port,
auto_search_port,
auto_start_server,
fetch_output_folder_from_a1111_settings,
] = gradio_img_browser_tab(config)
with gr.TabItem(label="Others", elem_id="cozy_nest_others_settings_tab"):
with gr.Column():
[
enable_clear_button,
enable_extra_network_tweaks
] = gradio_others_settings(config)
ui_action_btn(accent_color, accent_generate_button, bg_gradiant_color, card_height, card_width,
disable_waves_and_gradiant, error_popup, font_size, main_menu_position,
quicksettings_position, font_color, font_color_light, waves_color, disable_image_browser,
server_default_port,
auto_search_port,
auto_start_server,
fetch_output_folder_from_a1111_settings, sfw_mode, enable_clear_button,
enable_extra_network_tweaks)
"</div>")
# hidden field to store some useful data and trigger some server actions (like "send to" txt2img,...)
gradio_hidden_field(server_port)
# footer
gr.HTML(value="<div class='nevysha settings-nevyui-bottom'>"
" <p class='info'>Made by Nevysha with luv</p>"
"</div>", elem_id="nevyui_footer_wrapper")
return [(ui, "Nevysha Cozy Nest", "nevyui")]
def gradio_others_settings(config):
gr.HTML(value="<div id='cozynest_others_settings_header'>"
"<p>Those settings are heavy on DOM modification and might conflict with some others extensions</p>"
"<p>Reload UI needed to apply</p>"
"</div>")
enable_clear_button = gr.Checkbox(label="Enable clear gallery button in txt2img and img2img tabs",
value=config.get('enable_clear_button'), elem_id="cozynest_various_clearbtn")
enable_extra_network_tweaks = gr.Checkbox(label="Enable extra network tweaks",
value=config.get('enable_extra_network_tweaks'),
elem_id="cozynest_various_extra_network_tweaks")
return [enable_clear_button, enable_extra_network_tweaks]
cwd = os.path.normpath(os.path.join(__file__, "../../"))
async def send_to_socket(data, server_port):
async with websockets.connect(f'ws://localhost:{server_port}') as websocket:
try:
while True:
# Send data to the server
data = json.dumps(data).encode('utf-8')
await websocket.send(data)
# Receive response from the server
await websocket.recv()
await websocket.close()
break
except websockets.exceptions.ConnectionClosed:
pass
def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
app.mount(
"/cozy-nest-client/",
@ -662,6 +407,15 @@ def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
return {"message": "Config saved successfully"}
@app.delete("/cozy-nest/config")
async def delete_config():
reset_settings()
return {"message": "Config deleted successfully"}
@app.get("/cozy-nest/reloadui")
async def reload_ui():
request_restart()
@app.get("/cozy-nest/image")
async def get_image(path: str):
# Open the file in binary mode
@ -679,8 +433,101 @@ def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
return response
except FileNotFoundError:
tools.delete_img_data(path)
return Response(status_code=404, content="File not found")
@app.delete("/cozy-nest/image")
async def delete_image(path: str):
try:
os.remove(path)
tools.delete_img_data(path)
return {"message": "File deleted successfully"}
except FileNotFoundError:
return Response(status_code=404, content="File not found")
@app.delete("/cozy-nest/index")
async def delete_index():
global _server_port
config = get_dict_from_config()
cnib_output_folder = config.get('cnib_output_folder')
if cnib_output_folder and cnib_output_folder != "":
tools.delete_index()
def _scrap():
try:
data = tools.scrap_image_folders(cnib_output_folder)
asyncio.run(send_to_socket({
'what': 'index_built',
'data': data['images'],
}, _server_port))
finally:
pass
thread = threading.Thread(target=_scrap)
thread.start()
return {"message": "Index deleted successfully, rebuilding index in background"}
else:
return Response(status_code=412, content="Missing output folder in config")
@app.put('/cozy-nest/image')
async def move_image(request: Request, path: str):
try:
request_json = await request.json()
is_archive = request_json['archive']
if not is_archive:
# do nothing for now
return Response(status_code=501, content="unimplemented")
config = get_dict_from_config()
archive_path = config.get('archive_path')
if not archive_path or archive_path == "":
# return {"message": "archive path not set"}
return Response(status_code=412, content="archive path not set")
# check if archive path exists
if not os.path.exists(archive_path):
return Response(status_code=412, content=f"archive path:{archive_path} does not exist")
new_path = os.path.join(archive_path, os.path.basename(path))
os.rename(path, new_path)
tools.delete_img_data(path)
return {"message": "File moved successfully"}
except FileNotFoundError:
return Response(status_code=404, content="File not found")
@app.get("/cozy-nest/image-exif")
async def get_image_exif(path: str):
src_info = tools.get_image_exif(path)
return Response(content=json.dumps(src_info), media_type="application/json")
@app.post("/cozy-nest/image-exif")
async def set_image_exif(request: Request):
# Access POST parameters
request_json = await request.json()
data = request_json['data']
path = request_json['path']
image = Image.open(path)
image.load()
tgt_info = PngInfo()
for key, value in data.items():
tgt_info.add_text(key, str(value))
image.save(path, pnginfo=tgt_info)
tools.update_img_data(path)
return {"message": "EXIF data saved successfully"}
@app.get("/cozy-nest/extra-networks")
async def get_extra_networks():
pass
script_callbacks.on_ui_tabs(on_ui_tabs)
script_callbacks.on_app_started(cozy_nest_api)

138
scripts/tools.py Normal file
View File

@ -0,0 +1,138 @@
import hashlib
import json
import os
from PIL import Image
def get_image_exif(path: str):
try:
image = Image.open(path)
image.load()
src_info = image.text or {}
image.close()
return src_info
except:
return {}
def calculate_sha256(file_path):
# Create a SHA-256 hash object
sha256_hash = hashlib.sha256()
# Open the file in binary mode
with open(file_path, 'rb') as file:
# Read the file in chunks to avoid loading the entire file into memory
for chunk in iter(lambda: file.read(4096), b''):
# Update the hash object with the current chunk
sha256_hash.update(chunk)
# Get the hexadecimal representation of the hash digest
sha256_hex = sha256_hash.hexdigest()
return sha256_hex
def get_exif(path):
# info = image.info
exif = get_image_exif(path)
# get the image sha256 hash
sha256_hex = calculate_sha256(path)
img = {
'path': path,
'hash': sha256_hex,
'metadata': {
'date': os.path.getmtime(path),
'exif': exif,
}
}
return img
def update_img_data(path):
# get the corresponding image in the cache, update its metadata and save it back to the cache in the same position
with open(CACHE_FILENAME, 'r') as f:
cache = json.loads(f.read())
for img in cache['images']:
if img['path'] == path:
exif = get_image_exif(path)
img['metadata'] = {
'date': os.path.getmtime(path),
'exif': exif,
}
break
with open(CACHE_FILENAME, 'w') as fw:
fw.write(json.dumps(cache, indent=4))
def delete_img_data(path):
# get the corresponding image in the cache and remove it
with open(CACHE_FILENAME, 'r') as f:
cache = json.loads(f.read())
for img in cache['images']:
if img['path'] == path:
cache['images'].remove(img)
break
with open(CACHE_FILENAME, 'w') as fw:
fw.write(json.dumps(cache))
def delete_index():
# delete the cache file
if os.path.exists(CACHE_FILENAME):
os.remove(CACHE_FILENAME)
EXTENSION_TECHNICAL_NAME = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# TODO use db instead of cache file
CACHE_FILENAME = f"extensions/{EXTENSION_TECHNICAL_NAME}/data/images.cache"
def scrap_image_folders(images_folders):
# if the cache file exists, read it and return the data
if os.path.exists(CACHE_FILENAME):
with open(CACHE_FILENAME, 'r') as f:
return json.loads(f.read())
# scrape the images folder recursively
# TODO store images as hash=>data in index
images = []
for images_folder in images_folders:
for root, dirs, files in os.walk(images_folder):
for file in files:
if file.endswith(".png"):
# get exif data
img = get_exif(os.path.join(root, file))
images.append(img)
# sort the images by date (newest first) metadata.date
images.sort(key=lambda x: x['metadata']['date'], reverse=True)
# send the images to the client
data = {
'what': 'images',
'images': images
}
if not os.path.exists(CACHE_FILENAME):
open(CACHE_FILENAME, 'w').close()
with open(CACHE_FILENAME, 'w') as f:
f.write(json.dumps(data))
return data
def new_image(data):
# Add the image to the cache
with open(CACHE_FILENAME, 'r') as f:
cache = json.loads(f.read())
cache['images'].insert(0, data)
with open(CACHE_FILENAME, 'w') as f:
f.write(json.dumps(cache))

View File

@ -1,3 +1,3 @@
{
"version": "2.1.7"
"version": "2.2.0"
}