pull/178/head
Nevysha 2023-07-04 23:02:34 +02:00 committed by GitHub
parent f639e770db
commit e2f49c361f
47 changed files with 3652 additions and 1633 deletions

3
.gitignore vendored
View File

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

View File

@ -3,9 +3,18 @@
- Automatic1111's webui 1.3.2 release.
- SD Next (Vlad's fork) Version: 4867dafa Fri Jun 23. (Not compatible with latest!)
## New features in 2.4.0
- [x] Dedicated Extra Network component which *should* be more stable and faster.
- [x] Compatible with Civitai Helper (and hard requirement to generate civitai.info file)
- [x] Search field
- [x] NSFW filter
- [x] Mark as NSFW
- [x] Folder tree view (toggleable)
- [x] Multithread image indexer for image browser
## Minor changes & fixes in 2.3.4
- [x] Compatibility with https://github.com/DominikDoom/a1111-sd-webui-tagcomplete (ctrl+space to autocomplete tags in Cozy Prompt)
- [x] Synthax color in prompt for wildcard (ie: '__devilkkw/body-1/eyes_iris_colors__')
- [x] Synthax color in prompt for wildcard (ie: '\_\_devilkkw/body-1/eyes_iris_colors\_\_')
- [x] Synthax color in prompt for attention value (':1.1', ':2.3', ...)
- [x] Keybinding to increase or decrease attention value (ctrl+up, ctrl+down)
@ -29,140 +38,3 @@
- [x] Move prompt tools button ("redo last prompt", ...)
- [x] Add a secondary accent color in settings, applied to some elements (open scripts...)
- [x] Reworked a lots of padding for a cleaner and more compact view
## Minor changes & fixes in 2.2.4
- [x] Fix image url which was hardcoded to port 7860.
- [x] use data_dir args to locate and save settings.
- [x] small space optimization.
## Minor changes & fixes in 2.2.3
- [x] Differed Extra Network loading. This mean that the initial loading of Cozy Nest should be faster.
## 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)
## Minor changes & fixes in 2.1.6
- [x] Some settings were not saved properly
## Minor changes & fixes in 2.1.5
- [x] Font color settings added (and auto calculate subdued color)
- [x] Cozy Nest should detect more easily if it is running on Automatic1111's webui or SD Next (Vlad's fork)
- [x] Few tweaks for SD Next
## Minor changes & fixes in 2.1.4
- [x] Add a "clear" button to txt2img and img2img gallery
- [x] When background and waves animation are disabled they now remain visible but static
- [x] Settings to enable/disable extra networks tweaks
- [x] Settings to enable/disable clear gallery button tweaks
- [x] Fix CozyNest=No not working
## Minor changes & fixes in 2.1.3
- [x] Fix for default "send to" button
- [x] Fix image browser search
- [x] SFW settings to blur all images in the UI 👀
- [x] Removed console spam from image browser
- [x] Faster animation for settings, update and side panels
- [x] Fix for troubleshot dialog with Vlad's fork
## Minor changes & fixes in 2.1.2
- [x] Fix for Vlad's fork compatibility
## Minor changes & fixes in 2.1.1
- [x] Fix bug for image with special characters in their name (like "+")
## New features in 2.1.0
- [x] Settings panel for image browser
- [x] Chose default socket port
- [x] Enable/disable auto search for free port
- [x] Enable/disable auto fetch for output folder from a1111 settings
- [x] Add/Remove custom output folder
- [x] Code has been refactored to use a module builder (Vite). Refactoring is still in progress in the long run. But it should help to stabilize the code base.
## Minor changes & fixes in 2.0.9
- [x] Using the builtin png info to get image generation data in image browser
- [x] Add error handling and display for image browser
- [x] Fix : prevent error if a tab button made by an extension does not have inner text
- [x] Add close button to Cozy Nest update panel
## Minor changes & fixes in 2.0.8
- [x] Remove the "click outside to close" behavior of the settings panel. It was causing some issue with others extensions.
## Minor changes & fixes in 2.0.7
- [x] Fix : small fix
## Minor changes & fixes in 2.0.5
- [x] Fix for menu and quicksettings size. Should be more consistent now, but still buggy with too many quicksettings.
## Minor changes & fixes in 2.0.4
- [x] Simplify extra network cards/thumb display - should work better. I know that a lots of work still remain to be done on this part but code need a cleanup before.
## Minor changes & fixes in 2.0.3
- [x] "Send to" buttons are opening right side drawer panels rather than swapping tabs #75
- [x] Fix: tweak of SDAtom-WebUi-client-queue-ext extension. To enable the tweak you have to add `"extensions": ["SDAtom-WebUi-client-queue-ext"]` to your nevyui_settings.json file
## Minor changes & fixes in 2.0.2
- [x] Cozy Nest error popup should now only be displayed if filename contains Cozy Nest
- [x] Fix image browser crash if metadata are not formatted as expected (although metadata display may display "Error parsing metadata")
- [x] Fix Download image link hidden on hover
- [x] Fix selectable options list passing under other elements
- [x] Fix: Grid image appear in Image browser after batch gen
- [x] Fix: Wave badly displayed
- [x] Add setting to disable waves and gradiant bg
## Minor changes & fixes in 2.0.1
- [x] Fix: Some user report a missing scrollbar in Extra network tab
- [x] Fix: image browser spamming console
- [x] Fix: Image Browser socket do not always close properly when reloading the UI
- [x] disable image browser via settings (NOW DISABLED BY DEFAULT)
- [x] extra network tab fix and perf (a bit)
## New features in 2.0.0
- [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] 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
## Issues fixed
- [x] Laggy Extra Network tab opening
- [x] crash when loading without setting file saved
- [x] Fix Drag and drop image
## Known Issue
- [ ] Metadata display in image browser may display "Error parsing metadata"
- [ ] Partial compatibility with Firefox and Opera GX
- [ ] Most tweak will not support a live window resize
- [ ] Some user report a missing scrollbar in Extra network tab
- [ ] Some user report an crash when attempting to open Extra Network tab

View File

@ -109,5 +109,6 @@ Feel free to contribute to this project. I'm sure there are a lot of things that
I'll try to keep this extension up to date with the latest version of auto1111.
# Credits
* [DominikDoom](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete) used part of is code to retrieve valid extra networks
* [anapnoe](https://github.com/anapnoe/stable-diffusion-webui-ux)'s incredible work on its fork of sd-webui
* [AUTOMATIC1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui)'s work on sd-webui

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1,2 @@
VITE_CONTEXT=DEV
VITE_CONTEXT=DEV
PROMPT_LOGGING=0

View File

@ -1 +1,2 @@
VITE_CONTEXT=PROD
VITE_CONTEXT=PROD
PROMPT_LOGGING=0

View File

@ -18,7 +18,11 @@ export const modalTheme = defineMultiStyleConfig({
marginLeft: 'auto',
transform: 'none',
maxWidth: 'fit-content',
background: 'none',
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)',
},
}),
'nevysha-confirm':

View File

@ -14,7 +14,7 @@ import {Column, Row} from "../main/Utils.jsx";
import {ButtonWithConfirmDialog} from "../chakra/ButtonWithConfirmDialog.jsx";
import DOM_IDS from "../main/dom_ids.js";
import {Range as AceRange} from "ace-builds/src-noconflict/ace";
import {CozyLogger} from "../main/CozyLogger.js";
import {CozyLoggerPrompt as CozyLogger} from "../main/CozyLogger.js";
// ace.config.setModuleUrl(
// "ace/mode/json_worker",
// 'cozy-nest-client/node_modules/ace-builds/src-noconflict/worker-json.js')
@ -27,7 +27,7 @@ ace.config.setModuleUrl(
const langTools = ace.require("ace/ext/language_tools");
export function App({parentId, containerId, tabId}) {
export function App({parentId, containerId, tabId, resolve}) {
let savedHeight = localStorage.getItem(`cozy-prompt-height-${containerId}`);
savedHeight = savedHeight ? parseInt(savedHeight) : 200;
@ -255,6 +255,10 @@ export function App({parentId, containerId, tabId}) {
enableBasicAutocompletion: true
})
setTimeout(() => {
resolve()
}, 200);
}
// Helper function to increment the item

View File

@ -4,7 +4,17 @@ import {App} from "./App.jsx";
import {ChakraProvider} from '@chakra-ui/react'
import {theme} from "../chakra/chakra-theme.ts";
export default function startCozyPrompt(parentId, containerId, tabId) {
export default async function startCozyPrompt(parentId, containerId, tabId) {
return new Promise((resolve, reject) => {
try {
_startCozyPrompt(parentId, containerId, tabId, resolve);
} catch (e) {
reject(e);
}
});
}
function _startCozyPrompt(parentId, containerId, tabId, resolve) {
//
if (!document.getElementById(parentId)) {
setTimeout(() => startCozyPrompt(), 200)
@ -21,7 +31,7 @@ export default function startCozyPrompt(parentId, containerId, tabId) {
ReactDOM.createRoot(document.getElementById(containerId)).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<App containerId={containerId} parentId={parentId} tabId={tabId}/>
<App containerId={containerId} parentId={parentId} tabId={tabId} resolve={resolve}/>
</ChakraProvider >
</React.StrictMode>,
)

View File

@ -0,0 +1,194 @@
import React, {useContext} from "react";
import {useEffect} from "react";
import {Loading} from "../image-browser/App.jsx";
import {Checkbox, Tab, TabList, TabPanel, TabPanels, Tabs} from "@chakra-ui/react";
import './CozyExtraNetworks.scss'
import {ExtraNetworksCard} from "./ExtraNetworksCard.jsx";
import {Column, Row, RowFullWidth} from "../main/Utils.jsx";
import {SvgForReact} from "../main/svg_for_react.jsx";
import {FolderTreeFilter} from "./FolderTreeFilter.jsx";
const nevyshaScrollbar = {
'&::-webkit-scrollbar': {
width: '5px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'transparent'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'var(--ae-primary-color)',
borderRadius: '20px',
},
}
const indexRef = []
export function CozyExtraNetworks() {
const [extraNetworks, setExtraNetworks] = React.useState([])
const [folders, setFolders] = React.useState([])
const [ready, setReady] = React.useState(false)
const [fullyLoaded, setFullyLoaded] = React.useState(false)
const [searchString, setSearchString] = React.useState('')
const [displayFolderFilter, setDisplayFolderFilter] = React.useState(false)
const [nsfwFilter, setNsfwFilter] = React.useState(false)
const [selectedTab, setSelectedTab] = React.useState(null)
useEffect(() => {
(async () => {
const response = await fetch('/cozy-nest/extra_networks')
if (response.status !== 200) {
CozyLogger.error('failed to fetch extra networks', response)
return;
}
const _enJson = await response.json()
const responseFolders = await fetch('/cozy-nest/extra_networks/folders')
if (responseFolders.status !== 200) {
CozyLogger.error('failed to fetch extra networks folders', responseFolders)
return;
}
const _folders = await responseFolders.json()
setFolders(_folders)
setExtraNetworks(_enJson)
setReady(true)
})()
}, [])
useEffect(() => {
if (!nsfwFilter) return;
if (fullyLoaded) return;
setReady(false);
(async () => {
const response = await fetch('/cozy-nest/extra_networks/full')
if (response.status === 200) {
const json = await response.json()
setExtraNetworks(json)
setReady(true)
setFullyLoaded(true)
setSelectedTab(indexRef[0])
}
else {
CozyLogger.error('failed to fetch full extra networks info', response)
}
})()
}, [nsfwFilter])
function buildExtraNetworks() {
const EnTabs = [];
const EnTabPanels = [];
const style = {
border: 'none',
height: '100%',
borderBottom: '1px solid var(--ae-input-border-color)',
borderTop: '1px solid var(--tab-nav-background-color-selected)',
};
Object.keys(extraNetworks).forEach((network, index) => {
let tabName = String(network);
indexRef.push(network)
if (network === 'embeddings') {
tabName = 'Textual Inversion'
}
EnTabs.push(
<Tab key={index}>{tabName}</Tab>
)
EnTabPanels.push(
<TabPanel css={nevyshaScrollbar} key={index} style={style}>
<div className="CozyExtraNetworksPanels">
{extraNetworks[network].map((item, index) => {
return (
<ExtraNetworksCard
key={item.path}
item={item}
searchString={searchString}
selectedFolder={selectedFolder}
nsfwFilter={nsfwFilter}/>
)
})}
</div>
</TabPanel>
)
})
return {EnTabs, EnTabPanels}
}
function onTabSelect(index) {
setSelectedTab(indexRef[index])
}
const [selectedFolder, setSelectedFolder] = React.useState(null)
function folderSelectHandler({element}) {
CozyLogger.debug('folderSelectHandler', {element})
if (element.name === 'all' || !element.metadata) {
setSelectedFolder(null)
return
}
setSelectedFolder(element.metadata.path)
}
const Ui = buildExtraNetworks()
const hasSubFolders = folders[selectedTab] && !folders[selectedTab].empty
return (
<div className="CozyExtraNetworks">
{!ready && <Loading label="Loading Extra Networks..."/>}
{ready &&
<Column style={{width: '100%'}}>
<textarea data-testid="textbox"
placeholder="Search..."
rows="1"
spellCheck="false"
data-gramm="false"
style={{resize: 'none', minHeight: '35px'}}
onChange={(e) => setSearchString(e.target.value)}/>
<RowFullWidth style={{margin:'3px 0'}}>
<Checkbox
isChecked={displayFolderFilter}
disabled={!hasSubFolders}
onChange={(e) => setDisplayFolderFilter(e.target.checked)}
>Display folder filter</Checkbox>
<div style={{flex:1}}/>
<button
onClick={() => setNsfwFilter(!nsfwFilter)}
title="WARNING : this will take time as it will compute the info of all extra networks"
className="btn-settings toggleNsfwFilter"
>Toggle sfw filter
<span className="sfwFilterInfo">{!nsfwFilter ? SvgForReact.eyeSlash : SvgForReact.eye }</span>
</button>
</RowFullWidth>
<Row style={{height: 'calc(100% - 90px)'}}>
{displayFolderFilter &&
<FolderTreeFilter
hasSubFolders={hasSubFolders}
folder={folders[selectedTab]}
forNetwork={selectedTab}
selectHandler={folderSelectHandler}/>
}
<Tabs variant='nevysha' onChange={onTabSelect}>
<TabList style={{backgroundColor: 'var(--tab-nav-background-color)'}}>
{Ui.EnTabs}
</TabList>
<TabPanels>
{Ui.EnTabPanels}
</TabPanels>
</Tabs>
</Row>
</Column>
}
</div>
)
}

View File

@ -0,0 +1,121 @@
#cozy-extra-network-react {
width: 100%;
max-width: 100%;
height: 100%;
}
.CozyExtraNetworks {
width: 100%;
display: flex;
height: 100%;
.chakra-tabs {
display: flex;
flex-direction: column;
flex: 1;
}
.chakra-tabs__tab-panels {
height: 100%;
}
}
.CozyExtraNetworksPanels {
display: flex;
flex-direction: row;
flex-wrap: wrap;
row-gap: 5px;
column-gap: 2px;
}
.CozyExtraNetworksCard {
background-color: var(--ae-input-bg-color);
border-radius: 3px;
}
.CozyExtraNetworksCard:hover {
filter: brightness(1.2);
}
.en-preview-wrapper {
position: relative;
width: var(--extra-network-card-width);
height: var(--extra-network-card-height);
overflow: hidden;
cursor: pointer;
display: flex;
flex-direction: column;
border: 1px solid var(--ae-input-border-color);
border-radius: 3px;
}
.en-preview-thumbnail {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100% !important;
object-fit: cover; /* Maintain aspect ratio while covering the entire container */
}
.en-preview-thumbnail.black {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
font-size: 15px;
}
.cozy-en-info {
position: absolute;
bottom: 0;
z-index: 2;
background-color: var(--block-background-fill);
opacity: 0.9;
width: 100%;
text-align: center;
padding: 4px;
}
.en-preview-name {
width: 100%;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.cozy-en-actions {
display: flex;
flex-direction: row;
justify-content: space-evenly;
height: 100%;
background-color: var(--ae-input-bg-color);
opacity: 0.8;
z-index: 2;
align-content: center;
flex-wrap: wrap;
}
.cozy-en-actions > button {
min-width: 25px;
}
.cozy-en-actions > button:hover {
color: var(--ae-primary-color);
}
.cozy-en-actions > button > svg {
fill: var(--nevysha-font-color);
height: 15px;
}
.cozy-en-actions > button > svg:hover {
fill: var(--ae-primary-color);
}
.CozyExtraNetworks .btn-settings {
background-color: var(--ae-input-bg-color) !important;
width: 150px;
height: 20px;
}
span.sfwFilterInfo {
fill: var(--nevysha-font-color);
margin-left: 5px;
}
button.toggleNsfwFilter {
display: flex;
flex-direction: row;
align-items: center;
}

View File

@ -0,0 +1,341 @@
import React, {useEffect} from "react";
import {SvgForReact} from "../main/svg_for_react.jsx";
import {LazyComponent} from "./LazyComponent.jsx";
import {ImageUploadModal} from "./ImageUploadModal.jsx";
import {Button} from "@chakra-ui/react";
import CozyModal from "../main/modal/Module.jsx";
const CIVITAI_URL = {
"modelPage":"https://civitai.com/models/",
"modelId": "https://civitai.com/api/v1/models/",
"modelVersionId": "https://civitai.com/api/v1/model-versions/",
"hash": "https://civitai.com/api/v1/model-versions/by-hash/"
}
function NsfwButton({onClick, nsfw}) {
const [isHovered, setIsHovered] = React.useState(false)
function getIcon() {
let iconName;
if (nsfw) {
iconName = isHovered ? "eye" : "eyeSlash"
}
else {
iconName = isHovered ? "eyeSlash" : "eye"
}
return SvgForReact[iconName];
}
return <button
title={nsfw ? "Mark as SFW" : "Mark as NSFW"}
onClick={onClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{getIcon()}
</button>;
}
export function ExtraNetworksCard({item, searchString, selectedFolder, nsfwFilter}) {
const [isHovered, setIsHovered] = React.useState(false)
const [info, setInfo] = React.useState(item.info || {})
const [infoLoaded, setInfoLoaded] = React.useState(item.info !== undefined)
const [validInfo, setValidInfo] = React.useState(info !== undefined && info !== null)
const [matchFilter, setMatchFilter] = React.useState(true)
const [showFileUpload, setShowFileUpload] = React.useState(false)
useEffect(() => {
if (infoLoaded || !isHovered) return
(async () => {
const response = await fetch(`/cozy-nest/extra_network?path=${encodeURIComponent(item.path)}`)
if (response.status !== 200) {
CozyLogger.error('Failed to fetch extra network info', response)
return
}
const info = await response.json()
setInfo(info)
setInfoLoaded(true)
setValidInfo(info !== null)
})();
}, [isHovered])
useEffect(() => {
if (nsfwFilter
&& info
&& info.model
&& info.model.nsfw) {
setMatchFilter(false)
return;
}
setMatchFilter(filterCard(searchString, selectedFolder))
}, [selectedFolder, searchString, nsfwFilter, info])
useEffect(() => {
//when hiding card, reset hover state
if (!matchFilter) {
setIsHovered(false)
}
//when showing card, reset hover state
if (showFileUpload) {
setIsHovered(false)
}
}, [matchFilter, showFileUpload])
function isNsfw() {
return info && info.model && info.model.nsfw
}
function filterCard(searchString, selectedFolder) {
const hasSelectFolder = selectedFolder && selectedFolder !== ''
function normalizePath(path) {
return path.replace(/[\/\\:]/g, '')
}
if (searchString !== '' || hasSelectFolder) {
//only search string
if (!hasSelectFolder) {
return normalizePath(item.path).includes(searchString)
}
//only selected folder
else if (searchString === '') {
return normalizePath(item.path).includes(normalizePath(selectedFolder))
}
//both
return (normalizePath(item.path).includes(searchString)
|| normalizePath(item.path).includes(normalizePath(selectedFolder)))
}
return true
}
function addTriggerWordsToPrompt(event) {
event.preventDefault();
event.stopPropagation();
if (info.trainedWords.length === 0) return
setAndPropagatePrompt(`${info.trainedWords.join(', ')}, `)
}
function getActiveTextarea(negativePrompt) {
const currentTab = get_uiCurrentTabContent().id
let textarea = null
if (currentTab.includes('txt2img')) {
if (negativePrompt) {
textarea = document.querySelector(`#txt2img_neg_prompt label textarea`)
} else
textarea = document.querySelector(`#txt2img_prompt label textarea`)
} else if (currentTab.includes('img2img')) {
if (negativePrompt) {
textarea = document.querySelector(`#img2img_neg_prompt label textarea`)
} else
textarea = document.querySelector(`#img2img_prompt label textarea`)
}
return textarea;
}
function clearPrompt(negativePrompt) {
let textarea = getActiveTextarea(negativePrompt);
textarea.value = ''
}
function setAndPropagatePrompt(newValue, negativePrompt) {
if (!newValue || newValue.length === 0) return
let textarea = getActiveTextarea(negativePrompt);
let value = textarea.value
if (value.length !== 0) {
value = `${textarea.value}\n${newValue}`
}
else {
value = newValue
}
textarea.value = value
//trigger input event
const event = new Event('input')
textarea.dispatchEvent(event)
}
function openCivitai(event) {
event.preventDefault();
event.stopPropagation();
if (!info.modelId) return
const url = `${CIVITAI_URL.modelPage}/${info.modelId}`
window.open(url, '_blank')
}
function usePromptFromPreview(event) {
event.preventDefault();
event.stopPropagation();
// image prompt are in info.images[?].meta.prompt (if any)
// go through all images until we find one with a prompt
if (info.images) {
for (const image of info.images) {
if (image.meta && image.meta.prompt) {
clearPrompt()
setAndPropagatePrompt(`${image.meta.prompt}, `)
if (image.meta.negativePrompt) {
clearPrompt(true)
setAndPropagatePrompt(`${image.meta.negativePrompt}, `, true)
}
return
}
}
}
CozyModal.showToast('warning', 'Not available', 'No prompt found in preview')
}
function replaceImage(event) {
event.preventDefault();
event.stopPropagation();
setShowFileUpload(!showFileUpload)
}
function loadExtraNetwork(event) {
event.preventDefault();
event.stopPropagation();
if (item.type === 'ckp') {
selectCheckpoint(item.fullName)
}
else if (item.type === 'ti') {
setAndPropagatePrompt(`${item.name}, `)
}
else if (item.type === 'lora' || item.type === 'lyco' || item.type === 'hypernet') {
setAndPropagatePrompt(`<${item.type}:${item.name}:1.00>, `)
}
}
async function toggleNSFW(event) {
event.preventDefault();
event.stopPropagation();
//send POST request to toggle nsfw
const response = await fetch(`/cozy-nest/extra_network/toggle-nsfw`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: item.path,
})
})
if (response.status !== 200) {
CozyLogger.error('Failed to toggle nsfw', response)
return
}
const info = await response.json()
item.info = info
setInfo(info)
}
function onPreviewSaved(previewPath) {
//update preview path
item.previewPath = previewPath
setShowFileUpload(false)
}
const hasTriggerWords = info.trainedWords && info.trainedWords.length > 0;
const hasModelId = info.modelId !== undefined;
return (
<div
className="CozyExtraNetworksCard"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={loadExtraNetwork}
title={item.name || item}
style={{display: matchFilter ? 'flex' : 'none'}}
>
{matchFilter && <div className="en-preview-wrapper">
{isHovered && infoLoaded && validInfo &&
<div className="cozy-en-actions">
<button
title="Replace preview image"
onClick={replaceImage}
>
{SvgForReact.image}
</button>
{hasModelId && <button
title="Open model in civitai"
onClick={openCivitai}
>
{SvgForReact.link}
</button>}
{hasTriggerWords && <button
title="Add trigger words to prompt"
onClick={addTriggerWordsToPrompt}
>
{SvgForReact.magicWand}
</button>}
<button
title="Use prompt from preview image"
onClick={usePromptFromPreview}
>
{SvgForReact.arrow}
</button>
<NsfwButton onClick={toggleNSFW} nsfw={isNsfw()}/>
</div>
}
{item.previewPath &&
<LazyComponent placeholderClassName="en-preview-thumbnail">
<img
className="en-preview-thumbnail"
src={`./sd_extra_networks/thumb?filename=${encodeURIComponent(item.previewPath)}&amp;mtime=${new Date().getTime()}`}
alt={item.name}
/>
</LazyComponent>
}
{!item.previewPath &&
<div className="en-preview-thumbnail black">
No preview
</div>
}
{showFileUpload &&
<div style={{zIndex:4}}>
<ImageUploadModal
visible={showFileUpload}
name={item.name}
path={item.path}
cancel={
<Button
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setShowFileUpload(false)
}}
>Cancel</Button>
}
callback={onPreviewSaved}
>
</ImageUploadModal>
</div>
}
<div className="cozy-en-info">
<div className="en-preview-name">{item.name || item}</div>
</div>
</div>}
</div>
);
}

View File

@ -0,0 +1,126 @@
import React from "react";
import { FaRegFolder, FaRegFolderOpen } from "react-icons/fa";
import { CiFolderOff } from "react-icons/ci";
import TreeView, { flattenTree } from "react-accessible-treeview";
import './FolderTreeFilter.scss'
class StoreClass {
constructor() {
// this.networks = {
// selectedNodes: [],
// };
this.networks = new Map();
//load from localstorage
const _networks = localStorage.getItem('CozyNest/FolderTreeFilter');
if (_networks) {
this.networks = new Map(Object.entries(JSON.parse(_networks)));
}
else {
this.networks.set('models', {selectedNodes: []});
this.networks.set('embeddings', {selectedNodes: []});
this.networks.set('lora', {selectedNodes: []});
this.networks.set('hypernetworks', {selectedNodes: []});
this.save();
}
}
selectNode(network, id) {
if (!this.networks.has(network)) {
this.networks.set(network, {selectedNodes: []});
}
this.networks.get(network).selectedNodes.push(id);
this.save();
}
unSelectNode(network, id) {
this.networks.get(network).selectedNodes = this.networks.get(network).selectedNodes.filter(_id => _id !== id);
this.save();
}
save() {
//save in localstorage?
// localStorage.setItem('CozyNest/FolderTreeFilter', JSON.stringify(Object.fromEntries(this.networks)));
}
}
const Store = new StoreClass();
export function FolderTreeFilter({hasSubFolders, folder, selectHandler, forNetwork}) {
//add a fake 'all' folder as first element of children
//check if first element is 'all'. If not, add it
const _folder = {...folder};
if (hasSubFolders && folder.children[0].name !== 'all') {
_folder.children = [{name: 'all', children: []}, ..._folder.children];
}
// load selected nodes from store
const selectedNodesName = Store.networks.get(forNetwork)?.selectedNodes || [];
const data = flattenTree(_folder);
const selectedNodes
= data.filter(node => selectedNodesName.includes(node.name)).map(node => node.id);
function onNodeSelect({element}) {
selectHandler({element})
}
function onExpand(branch) {
if (branch.isExpanded) {
Store.selectNode(forNetwork, branch.element.name);
}
else {
Store.unSelectNode(forNetwork, branch.element.name);
}
}
if (!hasSubFolders) {
return (
<div className="EmptyFolderTreeFilter"></div>
)
}
return (
<div className="FolderTreeFilter nevysha nevysha-scrollable">
<div className="directory">
<TreeView
data={data}
aria-label="directory tree"
onNodeSelect={onNodeSelect}
defaultExpandedIds={selectedNodes}
onExpand={onExpand}
nodeRenderer={({
element,
isBranch,
isExpanded,
getNodeProps,
level,
}) => (
<div {...getNodeProps()} style={{ paddingLeft: 20 * (level - 1) }}>
{isBranch ? (
<FolderIcon isOpen={isExpanded} />
) : (
<FileIcon filename={element.name} />
)}
{element.name}
</div>
)}
/>
</div>
</div>
);
}
const FolderIcon = ({ isOpen }) =>
isOpen ? (
<FaRegFolderOpen color="e8a87c" className="icon" />
) : (
<FaRegFolder color="e8a87c" className="icon" />
);
const FileIcon = () => <CiFolderOff color="var(--nevysha-font-color)" className="icon" />

View File

@ -0,0 +1,59 @@
.FolderTreeFilter {
min-width: 200px;
overflow-y: auto;
height: 100%;
margin-right: 10px;
.directory {
background: #242322;
font-family: monospace;
color: var(--nevysha-font-color);
user-select: none;
padding: 10px 0 10px 10px;
//margin: 0 8px 0 -8px;
}
.directory .tree,
.directory .tree-node,
.directory .tree-node-group {
list-style: none;
margin: 0;
padding: 0;
}
.directory .tree-branch-wrapper,
.directory .tree-node__leaf {
outline: none;
}
.directory .tree-node {
cursor: pointer;
display: flex;
flex-direction: row;
}
.directory .tree-node:hover {
background: rgba(255, 255, 255, 0.1);
}
.directory .tree .tree-node--focused {
background: rgba(255, 255, 255, 0.2);
}
.directory .tree .tree-node--selected {
background: var(--secondary-accent-color);
color: var(--secondary-accent-color-from-luminance)
}
.directory .tree-node__branch {
display: flex;
flex-direction: row;
}
.directory .icon {
vertical-align: middle;
padding-right: 5px;
font-size: calc(var(--nevysha-text-md) * 1.5);
}
}

View File

@ -0,0 +1,63 @@
.ImageUploadModal {
background-color: var(--ae-input-bg-color);
color: var(--nevysha-font-color);
padding: 15px;
border-radius: 3px;
border: 1px solid var(--ae-input-border-color);
.name {
display: flex;
flex-direction: column;
h1 {
display: flex;
flex-direction: row;
justify-content: center;
font-size: var(--nevysha-text-lg);
}
span {
margin-bottom: 25px;
display: flex;
flex-direction: row;
justify-content: center;
font-size: var(--nevysha-text-md);
filter: brightness(0.7);
}
}
label {
border: 2px dashed var(--secondary-accent-color);
svg {
path {
fill: var(--secondary-accent-color);
}
}
div {
span {
color: var(--nevysha-font-color);
filter: brightness(0.8);
}
}
}
.actions {
margin-top: 15px;
display: flex;
flex-direction: row;
justify-content: center;
gap: 15px;
button {
padding: 10px;
height: 25px;
border-radius: 1px;
border: 1px solid var(--ae-input-border-color) !important;
background: var(--ae-input-bg-color) !important;
color: var(--ae-input-color) !important;
font-size: var(--nevysha-text-md);
}
}
}

View File

@ -0,0 +1,66 @@
import React, {useEffect, useState} from "react";
import { FileUploader } from "react-drag-drop-files";
import "./ImageUpload.scss";
import {DialogWrapper} from "../settings/App.jsx";
import {RowFullWidth} from "../main/Utils.jsx";
import {Button} from "@chakra-ui/react";
const fileTypes = ["PNG"];
export function ImageUploadModal({visible, cancel, name, path, callback}) {
const [file, setFile] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const handleChange = (file) => {
setFile(file);
};
async function upload() {
setIsUploading(true);
const formData = new FormData();
formData.append("file", file);
formData.append("path", path);
const response = await fetch("/cozy-nest/extra_network/preview", {
method: "POST",
body: formData,
});
if (!response.ok) {
CozyLogger.error("Failed to upload image", response);
return;
}
callback((await response.json()).previewPath);
setIsUploading(false);
}
return (
<>
{visible &&
<DialogWrapper isVisible={isVisible}>
<div className="ImageUploadModal">
<div className="name">
<h1>Upload preview image</h1>
<span>{name}</span>
</div>
<FileUploader
handleChange={handleChange}
name="file"
types={fileTypes}
/>
<div className="actions">
{cancel}
<Button
isDisabled={!file && !isUploading}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
upload()
}}
>{!isUploading ? "Upload" : "Uploading..."}</Button>
</div>
</div>
</DialogWrapper>
}
</>
);
}

View File

@ -0,0 +1,35 @@
import React, {useEffect, useRef, useState} from "react";
export const LazyComponent = ({children, placeholderClassName}) => {
const [isInView, setIsInView] = useState(false);
const targetRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
},
{
root: null, // use viewport as root
rootMargin: '0px',
threshold: 0.1, // percentage of element's visibility needed to trigger the callback
}
);
if (targetRef.current) {
observer.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
observer.unobserve(targetRef.current);
}
};
}, []);
return (
<div ref={targetRef}>
{isInView ? children : <div className={placeholderClassName}>blank</div>}
</div>
);
};

View File

@ -0,0 +1,36 @@
import React from "react";
import ReactDOM from "react-dom/client";
import {CozyLogger} from "../main/CozyLogger.js";
import {CozyExtraNetworks} from "./CozyExtraNetworks.jsx";
import {ChakraProvider} from '@chakra-ui/react'
import {theme} from "../chakra/chakra-theme.ts";
import {hideNativeUiExtraNetworkElement} from "../main/cozy-utils.js";
export function startCozyExtraNetwork() {
return new Promise((resolve, reject) => {
_startExtraNetwork(resolve)
})
}
function _startExtraNetwork(resolve) {
CozyLogger.debug('startExtraNetwork')
if (!document.getElementById(`cozy-extra-network-react`)) {
CozyLogger.debug('waiting for extra network react')
setTimeout(() => _startExtraNetwork(), 200)
return
}
resolve()
hideNativeUiExtraNetworkElement('txt2img')
hideNativeUiExtraNetworkElement('img2img')
ReactDOM.createRoot(document.getElementById(`cozy-extra-network-react`)).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<CozyExtraNetworks />
</ChakraProvider>
</React.StrictMode>,
)
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import {CozyLogger} from "../main/CozyLogger.js";
import DOM_IDS from "../main/dom_ids.js";
import CozyNestEventBus from "../CozyNestEventBus.js";
import {hideNativeUiExtraNetworkElement} from "../main/cozy-utils.js";
export const LoaderContext = React.createContext({
ready: false,
@ -28,13 +29,6 @@ function observeDivChanges(targetDiv, prefix) {
});
}
function hideNativeUiElement(prefix) {
const triggerButton = document.querySelector(`button#${DOM_IDS.get('extra_networks_btn')(prefix)}`)
triggerButton.style.display = 'none'
const tabs = document.querySelector(`div#${prefix}_extra_networks`)
tabs.style.display = 'none';
}
async function requireNativeBloc(prefix) {
const triggerButton = document.querySelector(`button#${DOM_IDS.get('extra_networks_btn')(prefix)}`)
@ -74,7 +68,7 @@ export function LoaderProvider({children, prefix}) {
}
React.useEffect(() => {
hideNativeUiElement(prefix)
hideNativeUiExtraNetworkElement(prefix)
CozyNestEventBus.once(`extra_network-open:${prefix}`, (p) => {
CozyLogger.debug(`extra network load event received for ${p}`)

View File

@ -282,6 +282,7 @@ function App() {
rows="1"
spellCheck="false"
data-gramm="false"
style={{resize: 'none'}}
onChange={(e) => setSearchStr(e.target.value)}/>
<CozyTagsSelect setActiveTags={setActiveTags} />
</Row>

View File

@ -71,7 +71,7 @@ export function ImagesProvider({ children }: { children: ReactNode[] }) {
const updateExifInState = (image: Image) => {
const {metadata: {exif, hash}} = image
const {metadata: {exif}, hash} = image
const newImages = images.map(image => {
if (image.hash === hash) {
image.metadata.exif = exif

View File

@ -14,10 +14,12 @@ import startCozyPrompt from "./cozy-prompt/main.jsx";
import {startExtraNetwork} from "./extra-network/main.jsx";
import {OverrideUiJs} from "./main/override_ui.js";
import CozyNestEventBus from "./CozyNestEventBus.js";
import {startCozyExtraNetwork} from "./cozy_extra_network/main.jsx";
window.CozyTools = {
dummyLoraCard,
dummyControlNetBloc,
dummySubdirs
dummySubdirs,
stop:() => setTimeout(function(){debugger;}, 5000),
}
@ -28,10 +30,9 @@ export default async function cozyNestLoader() {
await cozyNestModuleLoader(async () => {
startCozyNestSettings();
if (COZY_NEST_CONFIG.enable_cozy_prompt === true) {
startCozyPrompt('txt2img_prompt', 'cozy_nest_prompt_txt2img', 'txt2img');
startCozyPrompt('img2img_prompt', 'cozy_nest_prompt_img2img', 'img2img');
await startCozyPrompt('txt2img_prompt', 'cozy_nest_prompt_txt2img', 'txt2img');
await startCozyPrompt('img2img_prompt', 'cozy_nest_prompt_img2img', 'img2img');
OverrideUiJs.override_confirm_clear_prompt();
}
@ -39,6 +40,9 @@ export default async function cozyNestLoader() {
await startExtraNetwork('txt2img')
await startExtraNetwork('img2img')
}
if (COZY_NEST_CONFIG.enable_cozy_extra_networks === true) {
await startCozyExtraNetwork()
}
startCozyNestImageBrowser();
@ -54,19 +58,9 @@ window.cozyNestLoader = cozyNestLoader;
return
}
// if (getTheme() === 'dark') {
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(sheet);
document.adoptedStyleSheets = [styleSheet];
// }
// else {
// const {latte} = await import('./main/latte.css?inline');
// const styleSheet = new CSSStyleSheet();
// styleSheet.replaceSync(sheet);
// const latteSheet = new CSSStyleSheet();
// latteSheet.replaceSync(latte);
// document.adoptedStyleSheets = [styleSheet, latteSheet];
// }
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(sheet);
document.adoptedStyleSheets = [styleSheet];
SimpleTimer.time(COZY_NEST_GRADIO_LOAD_DURATION);

View File

@ -1,43 +1,38 @@
export class CozyLogger {
static _instance = null;
class CozyLoggerClass {
static init(enabled) {
if (!CozyLogger._instance) {
CozyLogger._instance = new CozyLogger(enabled);
}
return CozyLogger._instance;
return new CozyLoggerClass(enabled);
}
static enable() {
CozyLogger._instance.enabled = true;
enable() {
this.enabled = true;
}
static disable() {
CozyLogger._instance.enabled = false;
disable() {
this.enabled = false;
}
static group(name) {
if (CozyLogger._instance.enabled) {
group(name) {
if (this.enabled) {
console.group(name);
}
}
static groupEnd() {
if (CozyLogger._instance.enabled) {
groupEnd() {
if (this.enabled) {
console.groupEnd();
}
}
static debug(...args) {
if (CozyLogger._instance.enabled) {
debug(...args) {
if (this.enabled) {
console.log('CozyNest:DEBUG:',...args);
}
}
static log(...args) {
log(...args) {
console.log('CozyNest:',...args);
}
static error(...args) {
error(...args) {
console.error('CozyNest:',...args);
}
@ -46,11 +41,8 @@ export class CozyLogger {
}
}
if (import.meta.env.VITE_CONTEXT === 'DEV') {
CozyLogger.init(true);
}
else {
CozyLogger.init(false);
}
const isDev = import.meta.env.VITE_CONTEXT === 'DEV'
export const CozyLogger = CozyLoggerClass.init(isDev);
export const CozyLoggerPrompt = CozyLoggerClass.init(import.meta.env.PROMPT_LOGGING === 1);
window.CozyLogger = CozyLogger;

View File

@ -16,7 +16,14 @@ export function Row(props) {
}
export const RowFullWidth = (props) => {
return <Row {...props} style={{width: '100%', justifyContent: 'space-between', gap: '30px'}}/>
const style = {width: '100%', justifyContent: 'space-between', gap: '30px'}
if (props.style) {
Object.assign(style, props.style)
}
return <Row {...props} style={style}/>
}
//component to wrap flex column

View File

@ -1043,11 +1043,15 @@ canvas.nevysha {
background-color: var(--block-background-fill) !important;
width: 75vw;
right: 0;
height: calc(100% - (100px + var(--menu-top-height)));
height: calc(100% - (95px + var(--menu-top-height)));
top: calc(75px + var(--menu-top-height));
padding-right: 15px;
display: flex;
flex-direction: row;
border: 1px solid #3f3f3f;
}
.nevysha-light .slide-right-browser-panel {
border: 1px solid var(--ae-input-border-color);
}
.extra-network-subdirs {
overflow: scroll;
@ -1359,6 +1363,9 @@ textarea.nevysha-image-browser-folder {
.markdown-body > ul > li {
margin-bottom: 10px;
}
.markdown-body em {
font-style: italic;
}
/*Style to apply when #nevysha_other_tabs is empty*/

View File

@ -1,4 +1,5 @@
import {CozyLogger} from "./CozyLogger.js";
import DOM_IDS from "./dom_ids.js";
export const getTheme = (modeFromConfig) => {
modeFromConfig = modeFromConfig || COZY_NEST_CONFIG.color_mode
@ -96,6 +97,13 @@ export const hasCozyNestNo = () => {
return false;
}
export function hideNativeUiExtraNetworkElement(prefix) {
const triggerButton = document.querySelector(`button#${DOM_IDS.get('extra_networks_btn')(prefix)}`)
triggerButton.style.display = 'none'
const tabs = document.querySelector(`div#${prefix}_extra_networks`)
tabs.style.display = 'none';
}
//dummy method
export const dummyLoraCard = () => {
const container = document.querySelector("#txt2img_lora_cards");

View File

@ -1,354 +0,0 @@
:root {
--ctp-rosewater: #dc8a78;
--ctp-flamingo: #dd7878;
--ctp-pink: #ea76cb;
--ctp-mauve: #8839ef;
--ctp-red: #d20f39;
--ctp-maroon: #e64553;
--ctp-peach: #fe640b;
--ctp-yellow: #df8e1d;
--ctp-green: #40a02b;
--ctp-teal: #179299;
--ctp-sky: #04a5e5;
--ctp-sapphire: #209fb5;
--ctp-blue: #1e66f5;
--ctp-lavender: #7287fd;
--ctp-text: #4c4f69;
--ctp-subtext1: #5c5f77;
--ctp-subtext0: #6c6f85;
--ctp-overlay2: #7c7f93;
--ctp-overlay1: #8c8fa1;
--ctp-overlay0: #9ca0b0;
--ctp-surface2: #acb0be;
--ctp-surface1: #bcc0cc;
--ctp-surface0: #ccd0da;
--ctp-base: #eff1f5;
--ctp-mantle: #e6e9ef;
--ctp-crust: #dce0e8;
--shadow: #dbdfef;
--ctp-accent: var(--ctp-maroon);
color-scheme: light;
}
:root,
.dark {
--body-background-fill: var(--background-fill-primary);
--body-text-color: var(--ctp-subtext0);
--color-accent-soft: var(--ctp-surface0);
--background-fill-primary: var(--ctp-mantle);
--background-fill-secondary: var(--ctp-base);
--border-color-accent: var(--ctp-surface0);
--border-color-primary: var(--ctp-surface1);
--link-text-color-active: var(--ctp-subtext0);
--link-text-color: var(--ctp-subtext0);
--link-text-color-hover: var(--ctp-accent);
--link-text-color-visited: var(--ctp-subtext0);
--body-text-color-subdued: var(--ctp-subtext0);
--shadow-spread: 1px;
--block-background-fill: var(--ctp-mantle);
--block-border-color: var(--border-color-primary);
--block_border_width: None;
--block-info-text-color: var(--body-text-color-subdued);
--block-label-background-fill: var(--background-fill-secondary);
--block-label-border-color: var(--border-color-primary);
--block_label_border_width: None;
--block-label-text-color: var(--ctp-text);
--block_shadow: None;
--block_title_background_fill: None;
--block_title_border_color: None;
--block_title_border_width: None;
--block-title-text-color: var(--ctp-text);
--panel-background-fill: var(--background-fill-secondary);
--panel-border-color: var(--border-color-primary);
--panel_border_width: None;
--checkbox-background-color: var(--ctp-surface1);
--checkbox-background-color-focus: var(--checkbox-background-color);
--checkbox-background-color-hover: var(--checkbox-background-color);
--checkbox-background-color-selected: var(--ctp-accent);
--checkbox-border-color: var(--ctp-surface0);
--checkbox-border-color-focus: var(--ctp-overlay0);
--checkbox-border-color-hover: var(--ctp-surface0);
--checkbox-border-color-selected: var(--ctp-overlay0);
--checkbox-border-width: var(--input-border-width);
--checkbox-label-background-fill: var(--ctp-surface0);
--checkbox-label-background-fill-hover: var(--ctp-surface0);
--checkbox-label-background-fill-selected: var(
--checkbox-label-background-fill
);
--checkbox-label-border-color: var(--border-color-primary);
--checkbox-label-border-color-hover: var(--checkbox-label-border-color);
--checkbox-label-border-width: var(--input-border-width);
--checkbox-label-text-color: var(--body-text-color);
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
--error-background-fill: var(--background-fill-primary);
--error-border-color: var(--border-color-primary);
--error_border_width: None;
--error-text-color: var(--ctp-red);
--input-background-fill: var(--ctp-base);
--input-background-fill-focus: var(--input-background-fill);
--input-background-fill-hover: var(--input-background-fill);
--input-border-color: var(--border-color-primary);
--input-border-color-focus: var(--neutral-700);
--input-border-color-hover: var(--input-border-color);
--input_border_width: None;
--input-placeholder-color: var(--neutral-500);
--input_shadow: None;
--input-shadow-focus: 0 0 0 var(--shadow-spread) var(--neutral-700),
var(--shadow-inset);
--loader_color: None;
--slider_color: None;
--stat-background-fill: linear-gradient(
to right,
var(--primary-400),
var(--primary-600)
);
--table-border-color: var(--neutral-700);
--table-even-background-fill: var(--neutral-950);
--table-odd-background-fill: var(--neutral-900);
--table-row-focus: var(--color-accent-soft);
--button-border-width: var(--input-border-width);
--button-cancel-background-fill: var(--ctp-red);
--button-cancel-background-fill-hover: var(--ctp-red);
--button-cancel-border-color: var(--ctp-red);
--button-cancel-border-color-hover: var(--button-cancel-border-color);
--button-cancel-text-color: var(--ctp-base);
--button-cancel-text-color-hover: var(--button-cancel-text-color);
--button-primary-background-fill: var(--ctp-accent);
--button-primary-background-fill-hover: var(--ctp-accent);
--button-primary-border-color: var(--ctp-accent);
--button-primary-border-color-hover: var(--button-primary-border-color);
--button-primary-text-color: var(--ctp-base);
--button-primary-text-color-hover: var(--button-primary-text-color);
--button-secondary-background-fill: var(--ctp-surface0);
--button-secondary-background-fill-hover: var(--ctp-surface0);
--button-secondary-border-color: var(--ctp-surface0);
--button-secondary-border-color-hover: var(--button-secondary-border-color);
--button-secondary-text-color: var(--ctp-text);
--button-secondary-text-color-hover: var(--button-secondary-text-color);
}
.gradio-button:hover {
filter: brightness(130%);
}
input[type='range']::-ms-track {
background: transparent;
}
input[type='range']::-ms-fill-lower {
background: var(--ctp-accent);
border-radius: 10px;
}
input[type='range']::-ms-fill-upper {
background: var(--ctp-overlay0);
border-radius: 10px;
}
input[type='range']:focus::-ms-fill-lower {
background: var(--ctp-accent);
}
input[type='range']:focus::-ms-fill-upper {
background: var(--ctp-overlay0);
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
.gr-box > div > div > input.gr-text-input {
width: 4em;
}
.progressDiv .progress {
background: var(--ctp-accent);
color: var(--shadow);
}
.dark .progressDiv,
.progressDiv {
background-color: var(--ctp-surface0);
}
input[type='range'] {
font-size: 1.5rem;
}
input[type='range'] {
color: var(--accent);
--thumb-height: 0.5em;
--track-height: 0.125em;
--track-color: var(--ctp-surface0);
--brightness-hover: 130%;
--brightness-down: 80%;
--clip-edges: 0.125em;
}
input[type='range'].win10-thumb {
color: var(--ctp-accent);
--thumb-height: 0.5em;
--thumb-width: 0.5em;
--clip-edges: 0.0125em;
}
@media (prefers-color-scheme: dark) {
input[type='range'] {
color: var(--ctp-accent);
--track-color: var(--ctp-surface0);
}
input[type='range'].win10-thumb {
color: var(--ctp-accent);
}
}
/* === range commons === */
input[type='range'] {
position: relative;
background: #fff0;
overflow: hidden;
}
input[type='range']:active {
cursor: grabbing;
}
input[type='range']:disabled {
filter: grayscale(1);
opacity: 0.3;
cursor: not-allowed;
}
/* === WebKit specific styles === */
input[type='range'],
input[type='range']::-webkit-slider-runnable-track,
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
transition: all ease 100ms;
height: var(--thumb-height);
}
input[type='range']::-webkit-slider-runnable-track,
input[type='range']::-webkit-slider-thumb {
position: relative;
}
input[type='range']::-webkit-slider-thumb {
--thumb-radius: calc((var(--thumb-height) * 0.5) - 1px);
--clip-top: calc((var(--thumb-height) - var(--track-height)) * 0.5 - 0.5px);
--clip-bottom: calc(var(--thumb-height) - var(--clip-top));
--clip-further: calc(100% + 1px);
--box-fill: calc(-100vmax - var(--thumb-width, var(--thumb-height))) 0 0
100vmax currentColor;
width: var(--thumb-width, var(--thumb-height));
background: linear-gradient(currentColor 0 0) scroll no-repeat left center /
50% calc(var(--track-height) + 1px);
background-color: currentColor;
box-shadow: var(--box-fill);
border-radius: var(--thumb-width, var(--thumb-height));
filter: brightness(100%);
clip-path: polygon(
100% -1px,
var(--clip-edges) -1px,
0 var(--clip-top),
-100vmax var(--clip-top),
-100vmax var(--clip-bottom),
0 var(--clip-bottom),
var(--clip-edges) 100%,
var(--clip-further) var(--clip-further)
);
}
input[type='range']:hover::-webkit-slider-thumb {
filter: brightness(var(--brightness-hover));
cursor: grab;
}
input[type='range']:active::-webkit-slider-thumb {
filter: brightness(var(--brightness-down));
cursor: grabbing;
}
input[type='range']::-webkit-slider-runnable-track {
background: linear-gradient(var(--track-color) 0 0) scroll no-repeat center /
100% calc(var(--track-height) + 1px);
}
input[type='range']:disabled::-webkit-slider-thumb {
cursor: not-allowed;
}
/* === Firefox specific styles === */
input[type='range'],
input[type='range']::-moz-range-track,
input[type='range']::-moz-range-thumb {
appearance: none;
transition: all ease 100ms;
height: var(--thumb-height);
}
input[type='range']::-moz-range-track,
input[type='range']::-moz-range-thumb,
input[type='range']::-moz-range-progress {
background: #fff0;
}
input[type='range']::-moz-range-thumb {
background: currentColor;
border: 0;
width: var(--thumb-width, var(--thumb-height));
border-radius: var(--thumb-width, var(--thumb-height));
cursor: grab;
}
input[type='range']:active::-moz-range-thumb {
cursor: grabbing;
}
input[type='range']::-moz-range-track {
width: 100%;
background: var(--track-color);
}
input[type='range']::-moz-range-progress {
appearance: none;
background: currentColor;
transition-delay: 30ms;
}
input[type='range']::-moz-range-track,
input[type='range']::-moz-range-progress {
height: calc(var(--track-height) + 1px);
border-radius: var(--track-height);
}
input[type='range']::-moz-range-thumb,
input[type='range']::-moz-range-progress {
filter: brightness(100%);
}
input[type='range']:hover::-moz-range-thumb,
input[type='range']:hover::-moz-range-progress {
filter: brightness(var(--brightness-hover));
}
input[type='range']:active::-moz-range-thumb,
input[type='range']:active::-moz-range-progress {
filter: brightness(var(--brightness-down));
}
input[type='range']:disabled::-moz-range-thumb {
cursor: not-allowed;
}
/* all of the input range stuff is from this guy*/
/* Shout out to them https://codepen.io/ShadowShahriar/pen/zYPPYrQ */

View File

@ -0,0 +1,112 @@
import React, {useEffect} from "react";
import {EventBus} from "./Module.jsx";
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Button,
useDisclosure, useToast
} from "@chakra-ui/react";
export function CozyModalSimple() {
const listen = 'CozyModalSimple'
const { isOpen, onOpen, onClose } = useDisclosure()
const [text, setText] = React.useState('')
useEffect(() => {
// listen to events on listen
const _eventFn = ({msg}) => {
setText(msg)
onOpen()
}
EventBus.on(listen, _eventFn)
return () => {
// unlisten to events on listen
EventBus.off(listen, _eventFn)
}
}, [])
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
<p>{text}</p>
</ModalBody>
<ModalFooter>
<Button colorScheme='blue' mr={3} onClick={onClose}>
Close
</Button>
<Button variant='ghost'>Secondary Action</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}
export function CozyModalRich() {
const listen = 'CozyModalRich'
useEffect(() => {
// listen to events on listen
const _eventFn = (args) => {
CozyLogger.debug("CozyModalRoot event", listen, args);
}
EventBus.on(listen, _eventFn)
return () => {
// unlisten to events on listen
EventBus.off(listen, _eventFn)
}
}, [])
return (
<div className="CozyModalRich">
</div>
)
}
export function CozyToast() {
const listen = 'CozyToast'
const toast = useToast()
useEffect(() => {
// listen to events on listen
const _eventFn = ({title, msg, status}) => {
toast({
title: title,
description: msg,
status: status,
duration: 9000,
isClosable: true,
})
}
EventBus.on(listen, _eventFn)
return () => {
// unlisten to events on listen
EventBus.off(listen, _eventFn)
}
}, [])
return (
<div className="CozyToast" />
)
}

View File

@ -0,0 +1,70 @@
import React from "react";
import ReactDOM from "react-dom/client";
import {ChakraProvider} from "@chakra-ui/react";
import {theme} from "../../chakra/chakra-theme.ts";
import {CozyModalRich, CozyModalSimple, CozyToast} from "./Modal.jsx";
import EventEmitter from 'eventemitter3';
class EventBusClass extends EventEmitter{
constructor() {
super();
}
}
export const EventBus = new EventBusClass();
/**
* Split Module and Modal.jsx to be able to use hmr
*/
let _ready = false;
function prepareReactHost() {
// insert a div at the end of the body
const _hostCozyModalSimple =
`<div id="CozyModalSimple"/>`;
const _hostCozyModalRich =
`<div id="CozyModalRich"/>`;
const _hostCozyToast =
`<div id="CozyToast"/>`;
document.body.insertAdjacentHTML("beforeend", _hostCozyModalSimple);
document.body.insertAdjacentHTML("beforeend", _hostCozyModalRich);
document.body.insertAdjacentHTML("beforeend", _hostCozyToast);
ReactDOM.createRoot(document.getElementById(`CozyModalSimple`)).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<CozyModalSimple />
</ChakraProvider>
</React.StrictMode>,
)
ReactDOM.createRoot(document.getElementById(`CozyModalRich`)).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<CozyModalRich />
</ChakraProvider>
</React.StrictMode>,
)
ReactDOM.createRoot(document.getElementById(`CozyToast`)).render(
<React.StrictMode>
<ChakraProvider theme={theme} >
<CozyToast />
</ChakraProvider>
</React.StrictMode>,
)
_ready = true;
}
let CozyModal = {
prepareReactHost,
showModalSimple: (msg) => EventBus.emit('CozyModalSimple', {msg}),
showModalRich: () => EventBus.emit('CozyModalRich'),
showToast: (status, title, msg) => EventBus.emit('CozyToast', {status, title, msg}),
};
//hook on modal event
window.ModalEventBus = EventBus;
window.CozyModal = CozyModal;
export default CozyModal

View File

@ -30,6 +30,7 @@ import clearGeneratedImage from './tweaks/clear-generated-image.js'
import {createAlertDiv, showAlert} from "./tweaks/cozy-alert.js";
import DOM_IDS from "./dom_ids.js";
import CozyNestEventBus from "../CozyNestEventBus.js";
import Modal from './modal/Module.jsx'
const addDraggable = ({prefix}) => {
@ -335,11 +336,11 @@ async function loadVersionData() {
//regex to replace [x] with a checkmark
const regex = /\[x\]/g;
remote_patchnote = remote_patchnote.replace(regex, ""); //TODO add icon ?
remote_patchnote = remote_patchnote.replace(regex, ""); //TODO NEVYSHA add icon ?
//regex to replace [ ] with a cross
const regex2 = /\[ \]/g;
remote_patchnote = remote_patchnote.replace(regex2, ""); //TODO add icon ?
remote_patchnote = remote_patchnote.replace(regex2, ""); //TODO NEVYSHA add icon ?
const converter = new showdown.Converter();
@ -609,7 +610,7 @@ function buildRightSlidePanelFor(label, buttonLabel, rightPanBtnWrapper, tab, pr
//create a panel to display Cozy Image Browser
const cozyImgBrowserPanel =
`<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 slide-right-browser-panel-container nevysha-scrollable ${label}_panel_inner">
<div class="nevysha" id="${label}-react"/>
</div>
</div>`;
@ -624,6 +625,11 @@ function buildRightSlidePanelFor(label, buttonLabel, rightPanBtnWrapper, tab, pr
if (cozyImgBrowserPanelWidth) {
cozyImgBrowserPanelWrapper.style.width = cozyImgBrowserPanelWidth;
}
else {
//get window width
const width = window.innerWidth;
cozyImgBrowserPanelWrapper.style.width = `${Math.round(width / 2)}px`;
}
cozyImgBrowserPanelWrapper.appendChild(lineWrapper)
//add a close button inside the line
@ -685,7 +691,7 @@ function buildRightSlidePanelFor(label, buttonLabel, rightPanBtnWrapper, tab, pr
function close() {
const panel = document.querySelector(`#${label}_panel`);
if (panel.style.display !== 'none') {
$(panel).animate({"margin-right": `-=${panel.offsetWidth}`}, 1, () => {
$(panel).animate({"margin-right": `-=${panel.offsetWidth}`}, 150, () => {
panel.style.display = 'none'
});
}
@ -726,7 +732,7 @@ function createRightWrapperDiv() {
tab.insertAdjacentElement('beforeend', rightPanBtnWrapper);
if (COZY_NEST_CONFIG.enable_extra_network_tweaks === true) {
buildRightSlidePanelFor('cozy-txt2img-extra-network', 'Extra Network'
buildRightSlidePanelFor('cozy-txt2img-extra-network', 'Extra Networks'
, rightPanBtnWrapper, tab, 'txt2img');
document.getElementById('cozy-txt2img-extra-network-react').classList.add('cozy-extra-network')
@ -735,6 +741,12 @@ function createRightWrapperDiv() {
document.getElementById('cozy-img2img-extra-network-react').classList.add('cozy-extra-network')
document.querySelector(`#cozy-img2img-extra-network_right_button`).style.display = 'none';
}
if (COZY_NEST_CONFIG.enable_cozy_extra_networks === true) {
//Cozy Nest reimplementation of extra networks
//if both are enabled, we use the Cozy Extra Networks label
const buttonLabel = COZY_NEST_CONFIG.enable_extra_network_tweaks ? 'Cozy Extra Networks' : 'Extra Networks';
buildRightSlidePanelFor('cozy-extra-network', buttonLabel, rightPanBtnWrapper, tab);
}
if (COZY_NEST_CONFIG.disable_image_browser !== true) {
buildRightSlidePanelFor('cozy-img-browser', 'Cozy Image Browser', rightPanBtnWrapper, tab);
}
@ -995,6 +1007,9 @@ const onLoad = (done, error) => {
// log time for onLoad execution after gradio has loaded
SimpleTimer.time(COZY_NEST_DOM_TWEAK_LOAD_DURATION);
// load modal module
Modal.prepareReactHost();
// check for gradio theme (vlad's fork)
if (document.querySelector('#setting_gradio_theme input')) {
const gradioTheme = document.querySelector('#setting_gradio_theme input').value
@ -1038,6 +1053,7 @@ const onLoad = (done, error) => {
//create a wrapper div on the right for slidable panels
createRightWrapperDiv();
let lastTab = get_uiCurrentTabContent().id;
onUiTabChange(() => {
CozyLogger.debug(`onUiTabChange newTab:${get_uiCurrentTabContent().id}, lastTab:${lastTab}`);
@ -1136,7 +1152,7 @@ const onLoad = (done, error) => {
if (window.location.href.includes('__theme')) {
showAlert(
"Warning",
"The __theme parameter is deprecated. Please use Cozy Nest settings instead.",
"The __theme parameter is deprecated for CozyNest. Please remove it from URL and use Cozy Nest settings instead.",
)
}

View File

@ -0,0 +1,15 @@
const rotate45 = {
transform: 'rotate(-45deg)',
}
const fillRed = {
fill: 'red',
}
export const SvgForReact = {
link: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z"/></svg>,
image: <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M448 80c8.8 0 16 7.2 16 16V415.8l-5-6.5-136-176c-4.5-5.9-11.6-9.3-19-9.3s-14.4 3.4-19 9.3L202 340.7l-30.5-42.7C167 291.7 159.8 288 152 288s-15 3.7-19.5 10.1l-80 112L48 416.3l0-.3V96c0-8.8 7.2-16 16-16H448zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm80 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>,
arrow: <svg style={rotate45} xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 288 480 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-370.7 0 73.4-73.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-128 128z"/></svg>,
magicWand: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><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>,
eye: <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 576 512"><path d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256c13.6 30 40.2 72.5 78.6 108.3C169.2 402.4 222.8 432 288 432s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80zM95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6zM288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80c-.7 0-1.3 0-2 0c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2c0 .7 0 1.3 0 2c0 44.2 35.8 80 80 80zm0-208a128 128 0 1 1 0 256 128 128 0 1 1 0-256z"/></svg>,
eyeSlash: <svg style={fillRed} xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 640 512"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm9.4 130.3C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5l-41.9-33zM192 256c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5z"/></svg>,
}

File diff suppressed because it is too large Load Diff

View File

@ -17,9 +17,12 @@
"eventemitter3": "^5.0.1",
"jquery": "^3.7.0",
"react": "^18.2.0",
"react-accessible-treeview": "^2.6.1",
"react-ace": "^10.1.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-drag-drop-files": "^2.3.10",
"react-icons": "^4.10.1",
"react-select": "^5.7.3",
"react-spinners": "^0.13.8",
"react-use-websocket": "^3.0.0",
@ -34,6 +37,7 @@
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"sass": "^1.63.6",
"typescript": "^5.0.4",
"vite": "^4.3.2"
}

View File

@ -1,5 +1,4 @@
.App {
/*TODO calc this*/
width: max(800px, 50vw);
height: fit-content;
min-height: 150px;

View File

@ -53,7 +53,7 @@ import {saveCozyNestConfig} from "../main/nevysha-cozy-nest.js";
import {ButtonWithConfirmDialog} from "../chakra/ButtonWithConfirmDialog.jsx";
function DialogWrapper({children, isVisible}) {
export function DialogWrapper({children, isVisible}) {
const { isOpen, onOpen, onClose } = useDisclosure({isOpen: isVisible})
const cancelRef = useRef()
@ -215,7 +215,7 @@ export function App() {
<Tab>Main Settings</Tab>
<Tab>Image Browser Settings</Tab>
<Tab>Cozy Prompt Settings</Tab>
<Tab>Others</Tab>
<Tab>Cozy Nest Modules</Tab>
</TabList>
<TabPanels>
@ -427,14 +427,22 @@ export function App() {
isChecked={!config.disable_image_browser}
onChange={(e) => setConfig({...config, disable_image_browser: !e.target.checked})}
>Enable image browser (Reload UI required)</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>
<span style={{marginTop: '25px'}}>Extra Networks</span>
<span>You probably only want one of those</span>
<Checkbox
isChecked={config.enable_extra_network_tweaks}
onChange={(e) => setConfig({...config, enable_extra_network_tweaks: e.target.checked})}
>Tweaks existing : just move existing component in side panel (will drop support soon)</Checkbox>
<Checkbox
isChecked={config.enable_cozy_extra_networks}
onChange={(e) => setConfig({...config, enable_cozy_extra_networks: e.target.checked})}
>Cozy Nest Extra Network new implementation</Checkbox>
</Column>
</TabPanel>
</TabPanels>

View File

@ -1,34 +0,0 @@
import os
import sys
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# add the CozyNest extension to the sys.path.
sys.path.append(parent_dir)
from scripts.cozynest_image_browser import start_server, start_server_in_dedicated_process
# call start_server()
# main
if __name__ == '__main__':
port = 3333
if len(sys.argv) < 1:
print("CozyNest: No images folder specified")
exit()
# get the images folder from arguments
# it can be any number of arguments, add all of them in the images_folders list
images_folders = []
for i in range(1, len(sys.argv)):
images_folders.append(sys.argv[i])
# start_server(images_folders, port)
start_server_in_dedicated_process(images_folders, port)
while True:
pass

View File

@ -1,34 +0,0 @@
import asyncio
import json
import socket
import websockets
async def connect_to_socket():
async with websockets.connect('ws://localhost:3333') as websocket:
try:
while True:
# Send data to the server
data = json.dumps({
'what': 'image_saved',
'data': {
'filename': "filename",
'pnginfo': "gen_params.pnginfo",
}
}).encode('utf-8')
await websocket.send(data)
# Receive response from the server
response = await websocket.recv()
print("Received response:", response)
websocket.close()
break
except websockets.exceptions.ConnectionClosed:
print("Connection to socket closed")
if __name__ == '__main__':
# Run the connection coroutine
asyncio.run(connect_to_socket())

View File

@ -0,0 +1,43 @@
import json
import os
from pathlib import Path
def is_log_enabled():
# check if the file log_enabled exists (in the same folder)
# if it does, then check log_enabled value (as json)
try:
log_config_file = Path(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'log_enabled'))
if log_config_file.is_file():
return True
except Exception:
return False
# This is a simple logger class that will be used to log messages stdout
# check the config if log is enabled
class CozyLoggerClass:
LOG_ENABLED = is_log_enabled()
def __init__(self, name: str):
self.name = name
self.log_enabled = CozyLoggerClass.LOG_ENABLED
def debug(self, message: str):
if self.log_enabled:
print(f"[{self.name}:DEBUG] {message}")
def warning(self, message: str):
print(f"[{self.name}:WARNING] {message}")
def info(self, message: str):
print(f"[{self.name}:INFO] {message}")
CozyLogger = CozyLoggerClass("Cozy")
if CozyLoggerClass.LOG_ENABLED:
CozyLogger.warning("Logger enabled. delete 'log_enabled' file to disable")
CozyLoggerExtNe = CozyLoggerClass("Cozy:ExtNe")
CozyLoggerConfig = CozyLoggerClass("Cozy:Config")
CozyLoggerImageBrowser = CozyLoggerClass("Cozy:ImageBrowser")

View File

@ -0,0 +1,167 @@
import json
import os
from modules import shared
from scripts.cozy_lib import Static
from scripts.cozy_lib.tools import output_folder_array
from scripts.cozy_lib.CozyLogger import CozyLoggerConfig
class CozyNestConfig:
def __init__(self):
config = self.get_dict_from_config()
self.config = {**CozyNestConfig.get_default_settings(), **config}
def init(self):
if self.config['webui'] == 'unknown' and hasattr(shared, 'get_version'):
version = shared.get_version()
# check if the 'app' is 'sd.next'
if version['app'] == 'sd.next':
self.config['webui'] = 'sd.next'
self.config['fetch_output_folder_from_a1111_settings'] = False
else:
self.config['webui'] = 'auto1111'
self.save_settings(self.config)
if self.config['webui'] == 'sd.next':
self.config['fetch_output_folder_from_a1111_settings'] = False
# check if cnib_output_folder is empty and/or need to be fetched from a1111 settings
cnib_output_folder = self.config.get('cnib_output_folder')
is_empty = cnib_output_folder == []
if not cnib_output_folder or is_empty:
cnib_output_folder = []
if self.config.get('fetch_output_folder_from_a1111_settings'):
# merge cnib_output_folder output_folder_array()
cnib_output_folder = cnib_output_folder + list(set(output_folder_array()) - set(cnib_output_folder))
self.config['cnib_output_folder'] = cnib_output_folder
# save the merged settings
self.save_settings(self.config)
def migrate(self):
current_version = CozyNestConfig.get_version()
current_version_code = CozyNestConfig.normalize_version(current_version)
local_version = self.config.get('version')
if not local_version:
local_version = '0.0.0'
local_version_code = CozyNestConfig.normalize_version(local_version)
if local_version_code < current_version_code:
CozyLoggerConfig.debug(f"current_version: {current_version} current_version_code: {current_version_code}")
CozyLoggerConfig.debug(f"local_version: {local_version} local_version_code: {local_version_code}")
if local_version_code < CozyNestConfig.normalize_version('2.4.0'):
self.config['enable_extra_network_tweaks'] = False
self.config['enable_cozy_extra_networks'] = True
self.config['version'] = current_version
self.simple_save_settings()
@staticmethod
def get_version():
with open(Static.VERSION_FILENAME, 'r') as f:
version = json.loads(f.read())
f.close()
current_version = version['version']
return current_version
@staticmethod
def normalize_version(version):
# normalize version ie 2.3.4 => 2003004
components = [int(x) for x in version.split(".")]
normalized_number = 0
for i, component in enumerate(components):
normalized_number += component * (100 ** (len(components) - i - 1))
return normalized_number
def get(self, key):
return self.config.get(key)
def simple_save_settings(self):
# create the file in extensions/Cozy-Nest if it doesn't exist
if not os.path.exists(Static.CONFIG_FILENAME):
open(Static.CONFIG_FILENAME, 'w').close()
# save each settings inside the file
with open(Static.CONFIG_FILENAME, 'w') as f:
f.write(json.dumps(self.config, indent=2))
f.close()
def save_settings(self, settings):
self.config = {
# always ensure that default settings for cross version compatibility
**CozyNestConfig.get_default_settings(),
**self.config,
**settings
}
self.simple_save_settings()
def get_dict_from_config(self):
if not os.path.exists(Static.CONFIG_FILENAME):
self.reset_settings()
# set version if config file was just created exist
self.config['version'] = CozyNestConfig.get_version(),
# return default config
return self.config
with open(Static.CONFIG_FILENAME, 'r') as f:
self.config = json.loads(f.read())
f.close()
return self.config
def reset_settings(self):
self.config = CozyNestConfig.get_default_settings()
self.simple_save_settings()
@staticmethod
def get_default_settings():
return {
'main_menu_position': 'top',
'accent_generate_button': True,
'font_size': 12,
'quicksettings_position': 'split',
'font_color': '#d4d4d4',
'font_color_light': rgb_to_hex(71, 71, 71),
'waves_color': rgb_to_hex(94, 26, 145),
'bg_gradiant_color': rgb_to_hex(101, 0, 94),
'accent_color': '#37b9dd',
'secondary_accent_color': '#b67ee1',
'card_height': '8',
'card_width': '16',
'error_popup': True,
'disable_image_browser': True,
'disable_waves_and_gradiant': False,
'server_default_port': 3333,
'auto_search_port': True,
'auto_start_server': True,
'fetch_output_folder_from_a1111_settings': False,
'cnib_output_folder': [],
'archive_path': '',
'sfw_mode': False,
'enable_clear_button': True,
'enable_extra_network_tweaks': False,
'enable_cozy_extra_networks': True,
'enable_cozy_prompt': True,
'carret_style': 'thin',
'save_last_prompt_local_storage': True,
'color_mode': 'dark',
'log_enabled': False,
'webui': 'unknown',
}
def rgb_to_hex(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
def hex_to_rgb(_hex):
rgb = []
for i in (0, 2, 4):
decimal = int(_hex[i:i + 2], 16)
rgb.append(decimal)
return tuple(rgb)

View File

@ -0,0 +1,6 @@
from pathlib import Path
EXTENSION_FOLDER = Path(__file__).parent.parent.parent
CONFIG_FILENAME = Path(EXTENSION_FOLDER, "nevyui_settings.json")
VERSION_FILENAME = Path(EXTENSION_FOLDER, "version_data.json")
CACHE_FILENAME = Path(EXTENSION_FOLDER, "data", "images.cache")

View File

@ -0,0 +1,504 @@
import glob
import json
import os
import shutil
from pathlib import Path
from fastapi import Response, Request
from modules import sd_hijack, shared, sd_models
from scripts.cozy_lib.CozyLogger import CozyLoggerExtNe
def format_path_array(paths, _type, validator):
all_paths = []
for path in paths:
if validator(path):
fullName = str(path.name)
name = str(path.name)[:str(path.name).rfind('.')]
previewPath = os.path.join(path.parent, str(name)) + ".preview.png"
if not os.path.exists(previewPath):
previewPath = None
all_paths.append({
"name": name,
"fullName": fullName,
"type": _type,
"path": str(path),
# preview path if it exists os.path.join(path.parent, str(path.name))}.preview.png
"previewPath": previewPath
})
return sorted(all_paths, key=lambda x: x['name'].lower())
# gather extra network folders
# credit to https://github.com/DominikDoom/a1111-sd-webui-tagcomplete
class CozyExtraNetworksClass:
def __init__(self):
try:
from modules.paths import extensions_dir, script_path
except ImportError:
extensions_dir = None
script_path = None
if extensions_dir is not None and script_path is not None:
# Webui root path
self.FILE_DIR = Path(script_path)
# The extension base path
self.EXT_PATH = Path(extensions_dir)
else:
# Webui root path
self.FILE_DIR = Path().absolute()
# The extension base path
self.EXT_PATH = self.FILE_DIR.joinpath('extensions')
self.EMB_PATH = Path(shared.cmd_opts.embeddings_dir)
self.HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir)
try:
self.LORA_PATH = Path(shared.cmd_opts.lora_dir)
except AttributeError:
self.LORA_PATH = None
try:
self.LYCO_PATH = Path(shared.cmd_opts.lyco_dir)
except AttributeError:
self.LYCO_PATH = None
try:
ckpt_dir = shared.cmd_opts.ckpt_dir or sd_models.model_path
self.MODEL_PATH = Path(ckpt_dir)
except AttributeError or TypeError:
self.MODEL_PATH = None
# print all paths
CozyLoggerExtNe.debug(f"FILE_DIR: {self.FILE_DIR}")
CozyLoggerExtNe.debug(f"EXT_PATH: {self.EXT_PATH}")
CozyLoggerExtNe.debug(f"EMB_PATH: {self.EMB_PATH}")
CozyLoggerExtNe.debug(f"HYP_PATH: {self.HYP_PATH}")
CozyLoggerExtNe.debug(f"LORA_PATH: {self.LORA_PATH}")
CozyLoggerExtNe.debug(f"LYCO_PATH: {self.LYCO_PATH}")
CozyLoggerExtNe.debug(f"MODEL_PATH: {self.MODEL_PATH}")
def get_hypernetworks(self):
"""Write a list of all hypernetworks"""
# Get a list of all hypernetworks in the folder
hyp_paths = [Path(h) for h in glob.glob(self.HYP_PATH.joinpath("**/*").as_posix(), recursive=True)]
return format_path_array(hyp_paths, 'hypernet', lambda x: x.suffix in {".pt"})
def get_lora(self):
"""Write a list of all lora"""
# Get a list of all lora in the folder
lora_paths = [Path(lo) for lo in glob.glob(self.LORA_PATH.joinpath("**/*").as_posix(), recursive=True)]
return format_path_array(lora_paths, 'lora', lambda x: x.suffix in {".safetensors", ".ckpt", ".pt"})
def get_lyco(self):
"""Write a list of all LyCORIS/LOHA from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris"""
# Get a list of all LyCORIS in the folder
lyco_paths = [Path(ly) for ly in glob.glob(self.LYCO_PATH.joinpath("**/*").as_posix(), recursive=True)]
return format_path_array(lyco_paths, 'lyco', lambda x: x.suffix in {".safetensors", ".ckpt", ".pt"})
def get_models(self):
models_paths = [Path(m) for m in glob.glob(self.MODEL_PATH.joinpath("**/*").as_posix(), recursive=True)]
# all_models = [str(m.name) for m in models_paths if m.suffix in {".ckpt", ".safetensors"}]
return format_path_array(models_paths, 'ckp', lambda x: x.suffix in {".ckpt", ".safetensors"})
def get_embeddings(self):
"""Write a list of all embeddings with their version"""
# Version constants
V1_SHAPE = 768
V2_SHAPE = 1024
emb_v1 = []
emb_v2 = []
results = []
try:
# Get embedding dict from sd_hijack to separate v1/v2 embeddings
emb_type_a = sd_hijack.model_hijack.embedding_db.word_embeddings
emb_type_b = sd_hijack.model_hijack.embedding_db.skipped_embeddings
# Get the shape of the first item in the dict
emb_a_shape = -1
emb_b_shape = -1
if len(emb_type_a) > 0:
emb_a_shape = next(iter(emb_type_a.items()))[1].shape
if len(emb_type_b) > 0:
emb_b_shape = next(iter(emb_type_b.items()))[1].shape
# Add embeddings to the correct list
if emb_a_shape == V1_SHAPE:
emb_v1 = list(emb_type_a.keys())
elif emb_a_shape == V2_SHAPE:
emb_v2 = list(emb_type_a.keys())
if emb_b_shape == V1_SHAPE:
emb_v1 = list(emb_type_b.keys())
elif emb_b_shape == V2_SHAPE:
emb_v2 = list(emb_type_b.keys())
for e in emb_v1:
emb_path = os.path.join(self.EMB_PATH, e)
previewPath = f"{emb_path}.preview.png"
results.append({
"name": e,
"version": "v1",
"type": "ti",
"path": f"{emb_path}.pt",
"previewPath": previewPath if os.path.isfile(previewPath) else None,
"parentFolder": os.path.join(self.EMB_PATH, e)
})
for e in emb_v2:
emb_path = os.path.join(self.EMB_PATH, e)
previewPath = f"{emb_path}.preview.png"
results.append({
"name": e,
"version": "v2",
"type": "ti",
"path": f"{emb_path}.pt",
"previewPath": previewPath if os.path.isfile(previewPath) else None,
"parentFolder": os.path.join(self.EMB_PATH, e)
})
results = sorted(results, key=lambda x: x["name"].lower())
except AttributeError:
print(
"tag_autocomplete_helper: Old webui version or unrecognized model shape, using fallback for embedding completion.")
# Get a list of all embeddings in the folder
all_embeds = [str(e.relative_to(self.EMB_PATH)) for e in self.EMB_PATH.rglob("*") if
e.suffix in {".bin", ".pt", ".png", '.webp', '.jxl', '.avif'}]
# Remove files with a size of 0
all_embeds = [e for e in all_embeds if self.EMB_PATH.joinpath(e).stat().st_size > 0]
# Remove file extensions
all_embeds = [e[:e.rfind('.')] for e in all_embeds]
results = [e + "," for e in all_embeds]
return results
def create_api_route(self, app):
@app.get("/cozy-nest/valid_extra_networks")
def valid_extra_networks():
valid = {}
if self.MODEL_PATH is not None:
valid["MODEL_PATH"] = self.MODEL_PATH
if self.EMB_PATH is not None:
valid["EMB_PATH"] = self.EMB_PATH
if self.HYP_PATH is not None:
valid["HYP_PATH"] = self.HYP_PATH
if self.LORA_PATH is not None:
valid["LORA_PATH"] = self.LORA_PATH
if self.LYCO_PATH is not None:
valid["LYCO_PATH"] = self.LYCO_PATH
return valid
@app.get("/cozy-nest/extra_networks")
def extra_networks():
# get all extra networks by walking through all directories recursively
result = {}
if self.MODEL_PATH is not None:
model = self.get_models()
result["models"] = model
if self.EMB_PATH is not None:
emb = self.get_embeddings()
result["embeddings"] = emb
if self.HYP_PATH is not None:
hyp = self.get_hypernetworks()
result["hypernetworks"] = hyp
if self.LORA_PATH is not None:
lora = self.get_lora()
result["lora"] = lora
if self.LYCO_PATH is not None:
lyco = self.get_lyco()
result["lyco"] = lyco
return result
@app.get("/cozy-nest/extra_networks/folders")
def extra_networks_folder():
folder_tree = {}
if self.MODEL_PATH is not None:
models = self.get_models()
folder_tree['models'] = build_main_folder_tree_for(self.MODEL_PATH, models)
if self.EMB_PATH is not None:
emb = self.get_embeddings()
folder_tree['embeddings'] = build_main_folder_tree_for(self.EMB_PATH, emb)
if self.HYP_PATH is not None:
hyp = self.get_hypernetworks()
folder_tree['hypernetworks'] = build_main_folder_tree_for(self.HYP_PATH, hyp)
if self.LORA_PATH is not None:
lora = self.get_lora()
folder_tree['lora'] = build_main_folder_tree_for(self.LORA_PATH, lora)
if self.LYCO_PATH is not None:
lyco = self.get_lyco()
folder_tree['lyco'] = build_main_folder_tree_for(self.LYCO_PATH, lyco)
return folder_tree
@app.get("/cozy-nest/extra_networks/full")
def extra_networks():
# get all extra networks by walking through all directories recursively
result = {}
if self.MODEL_PATH is not None:
model = self.get_models()
# for each model, get the info
for m in model:
try:
info = get_info(m["path"])
m["info"] = info
except InfoUnavailableException:
m["info"] = {
"empty": True
}
result["models"] = model
if self.EMB_PATH is not None:
emb = self.get_embeddings()
# for each embedding, get the info
for e in emb:
try:
info = get_info(e["path"])
e["info"] = info
except InfoUnavailableException:
e["info"] = {
"empty": True
}
result["embeddings"] = emb
if self.HYP_PATH is not None:
hyp = self.get_hypernetworks()
# for each hypernetwork, get the info
for h in hyp:
try:
info = get_info(h["path"])
h["info"] = info
except InfoUnavailableException:
h["info"] = {
"empty": True
}
result["hypernetworks"] = hyp
if self.LORA_PATH is not None:
lora = self.get_lora()
# for each lora, get the info
for lo in lora:
try:
info = get_info(lo["path"])
lo["info"] = info
except InfoUnavailableException:
lo["info"] = {
"empty": True
}
result["lora"] = lora
if self.LYCO_PATH is not None:
lyco = self.get_lyco()
# for each lyco, get the info
for ly in lyco:
try:
info = get_info(ly["path"])
ly["info"] = info
except InfoUnavailableException:
ly["info"] = {
"empty": True
}
result["lyco"] = lyco
return result
@app.get("/cozy-nest/extra_network/")
def extra_network(path: str):
try:
info = get_info(path)
return info
except InfoUnavailableException as e:
return Response(status_code=e.code, content=e.message)
@app.post("/cozy-nest/extra_network/preview")
async def extra_network_preview(request: Request):
# path and file are in body as FormData
try:
form = await request.form()
path = form["path"]
upload_file = form["file"]
except Exception:
return Response(status_code=405, content="Invalid request body")
if path is None or upload_file is None:
return Response(status_code=405, content="Invalid request body")
try:
file_type = upload_file.content_type[upload_file.content_type.rfind("/") + 1:]
valid = ["png"]
if file_type not in valid:
return Response(status_code=405, content="Invalid file type")
path = Path(f"{str(path)[:str(path).rfind('.')]}.preview.{file_type}")
# save the file
with open(path, "wb") as buffer:
shutil.copyfileobj(upload_file.file, buffer)
except Exception as e:
print(e)
return Response(status_code=500, content="Failed to create file")
return Response(status_code=200, content=json.dumps({
"previewPath": f"{path}"
}))
@app.post("/cozy-nest/extra_network/toggle-nsfw")
async def extra_network_info(request: Request):
try:
request_json = await request.json()
path = request_json["path"]
except Exception:
return Response(status_code=405, content="Invalid request body")
if path is None:
return Response(status_code=405, content="Invalid request body")
try:
info = get_info(path)
except InfoUnavailableException:
info = {}
info_file = get_civitai_info_path(path)
with open(info_file, 'w') as f:
json.dump(info, f)
# nsfw data is there : info.model.nsfw. change the value to false or true
# and create each layer if it does not exist
if "model" not in info or "nsfw" not in info["model"]:
if "model" not in info:
info["model"] = {}
if "nsfw" not in info["model"]:
# since default is considered false set it to true
info["model"]["nsfw"] = True
else:
info["model"]["nsfw"] = not info["model"]["nsfw"]
# save the info
info_file = get_civitai_info_path(path)
with open(info_file, 'w') as f:
json.dump(info, f)
return info
class InfoUnavailableException(Exception):
# add a code attribute to the exception
def __init__(self, message, code):
super().__init__(message)
self.message = message
self.code = code
def get_info(path: str):
path = get_civitai_info_path(path)
if not path.exists():
raise InfoUnavailableException("Info file not found", 404)
with open(path, 'r') as f:
try:
info = json.load(f)
except Exception:
raise InfoUnavailableException("Could not read info file", 500)
return info
def get_civitai_info_path(path):
return Path(path[:path.rfind('.')] + '.civitai.info')
def build_main_folder_tree_for(_main_path, main_items):
# walk through all models and build the folder tree structure from self.MODEL_PATH downwards
models_folder_tree = {
"name": "all",
"empty": True,
"children": []
}
for m in main_items:
# split the path into its parts
rel_path = Path(m["path"]).relative_to(_main_path)
path_parts = str(rel_path).split(os.sep)
if len(path_parts) == 1:
# if the model is in the root folder, skip it
continue
models_folder_tree["empty"] = False
# remove the last part, which is the filename
path_parts.pop(-1)
# get the folder tree
models_folder_tree = add_folder_to_tree(_main_path, models_folder_tree, path_parts)
return models_folder_tree
def add_folder_to_tree(full_path_to_leaf, folder_tree, path_parts):
# if there are no more parts, we are done
if len(path_parts) == 0:
return folder_tree
# get the first part of the path
part = path_parts.pop(0)
full_path_to_leaf = Path(full_path_to_leaf, part)
# check if the part is already in the folder tree
for child in folder_tree["children"]:
if child["name"] == part:
# if it is, add the rest of the path to the child
add_folder_to_tree(full_path_to_leaf, child, path_parts)
return folder_tree
# if the part is not in the folder tree, add it
folder_tree["children"].append({
"name": part,
"metadata": {
"path": str(full_path_to_leaf),
},
"children": [],
})
# add the rest of the path to the new child
add_folder_to_tree(full_path_to_leaf, folder_tree["children"][-1], path_parts)
return folder_tree

View File

@ -1,18 +1,10 @@
import asyncio
import json
import multiprocessing
import os
import threading
from PIL import Image
from PIL.ExifTags import TAGS
from modules import script_callbacks
import modules.extras
import modules.images
import websockets
from websockets.server import serve
from scripts import tools
from scripts.cozy_lib import tools
async def start_server(images_folders, server_port, stopper):
@ -21,11 +13,9 @@ async def start_server(images_folders, server_port, stopper):
CLIENTS = set()
async def handle_client(websocket, path):
try:
CLIENTS.add(websocket)
while True:
if stopper.is_set():
print(f"CozyNestSocket: Stopping socket server on localhost:{server_port}...")
break
@ -38,7 +28,8 @@ async def start_server(images_folders, server_port, stopper):
try:
res = await process(data)
except Exception as e:
print(f"CozyNestSocket: Error while processing data: {e}")
print(f"CozyNestSocket: Error while processing data: {data}")
print(e)
res = json.dumps({
'what': 'error',
'data': 'None',

217
scripts/cozy_lib/tools.py Normal file
View File

@ -0,0 +1,217 @@
import hashlib
import json
import os
import sys
import threading
import time
from PIL import Image
from modules import shared, scripts
from scripts.cozy_lib import Static
from scripts.cozy_lib.CozyLogger import CozyLoggerImageBrowser as Logger
def output_folder_array():
outdir_txt2img_samples = shared.opts.data['outdir_txt2img_samples']
outdir_img2img_samples = shared.opts.data['outdir_img2img_samples']
outdir_extras_samples = shared.opts.data['outdir_extras_samples']
base_dir = scripts.basedir()
# check if outdir_txt2img_samples is a relative path
if not os.path.isabs(outdir_txt2img_samples):
outdir_txt2img_samples = os.path.normpath(os.path.join(base_dir, outdir_txt2img_samples))
if not os.path.isabs(outdir_img2img_samples):
outdir_img2img_samples = os.path.normpath(os.path.join(base_dir, outdir_img2img_samples))
if not os.path.isabs(outdir_extras_samples):
outdir_extras_samples = os.path.normpath(os.path.join(base_dir, outdir_extras_samples))
images_folders = [
outdir_txt2img_samples,
outdir_img2img_samples,
outdir_extras_samples,
]
return images_folders
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(Static.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(Static.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(Static.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(Static.CACHE_FILENAME, 'w') as fw:
fw.write(json.dumps(cache))
def delete_index():
# delete the cache file
if os.path.exists(Static.CACHE_FILENAME):
os.remove(Static.CACHE_FILENAME)
def scrap_image_folders(images_folders):
# if the cache file exists, read it and return the data
if os.path.exists(Static.CACHE_FILENAME):
with open(Static.CACHE_FILENAME, 'r') as f:
return json.loads(f.read())
# scrape the images folder recursively
Logger.debug('Scraping images folders...')
# TODO NEVYSHA store images as hash=>data in index
# gather all the images paths
images_path = []
for images_folder in images_folders:
for root, dirs, files in os.walk(images_folder):
for file in files:
if file.endswith(".png") or file.endswith(".jpg") or file.endswith(".jpeg"):
images_path.append(os.path.join(root, file))
Logger.info(f"Creating images index for {len(images_path)} images...")
# get the exif data for each image
# split the images_path list into chunks and process each chunk in a separate thread
thread_count = os.cpu_count()
Logger.debug(f"Using {thread_count} threads to process the images...")
split_count = (len(images_path) + 1) // thread_count
splited = [images_path[i * split_count:(i + 1) * split_count] for i in range(thread_count)]
images = []
start_time = time.time()
def process_chunk(_chunk):
for i, path in enumerate(_chunk):
# get exif data
img = get_exif(path)
images.append(img)
# create a thread for each chunk
threads = []
for chunk in splited:
t = threading.Thread(target=process_chunk, args=(chunk,))
t.start()
threads.append(t)
# wait for all the threads to finish
# loop to check if the threads are done
while True:
elapsed_time = time.time() - start_time
if not any([t.is_alive() for t in threads]):
display_progress_bar(1, elapsed_time)
break
# Calculate progress and elapsed time
i = len(images)
progress = (i + 1) / len(images_path)
# Display progress bar with elapsed time and estimated time remaining
# only each ten images or if it's the last image
if i == len(images_path) - 1:
display_progress_bar(1, elapsed_time)
else:
display_progress_bar(progress, elapsed_time)
time.sleep(0.1)
# 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(Static.CACHE_FILENAME):
open(Static.CACHE_FILENAME, 'w').close()
with open(Static.CACHE_FILENAME, 'w') as f:
f.write(json.dumps(data))
return data
def new_image(_new_img_data):
# Add the image to the cache
with open(Static.CACHE_FILENAME, 'r') as fr:
cache = json.loads(fr.read())
cache['images'].insert(0, _new_img_data)
with open(Static.CACHE_FILENAME, 'w') as fw:
fw.write(json.dumps(cache))
def display_progress_bar(progress, elapsed_time):
bar_length = 40
filled_length = int(bar_length * progress)
bar = '' * filled_length + '-' * (bar_length - filled_length)
percentage = progress * 100
progress_bar = f'[Cozy:ImageBrowser:INFO] Indexing: |{bar}| {percentage:.1f}% Complete | Elapsed: {elapsed_time:.2f}s'
sys.stdout.write('\r' + progress_bar)
sys.stdout.flush()

View File

@ -6,155 +6,22 @@ import socket
import subprocess
import sys
import threading
from typing import Any
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 import tools
from scripts.cozynest_image_browser import start_server
def rgb_to_hex(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
def hex_to_rgb(hex):
rgb = []
for i in (0, 2, 4):
decimal = int(hex[i:i + 2], 16)
rgb.append(decimal)
return tuple(rgb)
# check parent folder name (2 level above) to ensure compatibility after repo rename
EXTENSION_TECHNICAL_NAME = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
CONFIG_FILENAME = f"extensions/{EXTENSION_TECHNICAL_NAME}/nevyui_settings.json"
CONFIG_FILENAME = os.path.join(shared.cmd_opts.data_dir, CONFIG_FILENAME)
def gradio_save_settings(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,
archive_path,
sfw_mode,
enable_clear_button,
enable_extra_network_tweaks,
):
settings = {
'main_menu_position': main_menu_position,
'quicksettings_position': quicksettings_position,
'accent_generate_button': accent_generate_button,
'font_size': font_size,
'font_color': font_color,
'font_color_light': font_color_light,
'waves_color': waves_color,
'bg_gradiant_color': bg_gradiant_color,
'accent_color': accent_color,
'card_height': card_height,
'card_width': card_width,
'error_popup': error_popup,
'disable_image_browser': disable_image_browser,
'disable_waves_and_gradiant': disable_waves_and_gradiant,
'server_default_port': server_default_port,
'auto_search_port': auto_search_port,
'auto_start_server': auto_start_server,
'fetch_output_folder_from_a1111_settings': fetch_output_folder_from_a1111_settings,
'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()
settings = {**current_config, **settings}
save_settings(settings)
def save_settings(settings):
# create the file in extensions/Cozy-Nest if it doesn't exist
if not os.path.exists(CONFIG_FILENAME):
open(CONFIG_FILENAME, 'w').close()
# save each settings inside the file
with open(CONFIG_FILENAME, 'w') as f:
f.write(json.dumps(settings, indent=2))
f.close()
def get_dict_from_config():
if not os.path.exists(CONFIG_FILENAME):
reset_settings()
# return default config
return get_default_settings()
with open(CONFIG_FILENAME, 'r') as f:
config = json.loads(f.read())
f.close()
return config
def get_default_settings():
return {
'main_menu_position': 'top',
'accent_generate_button': True,
'font_size': 12,
'quicksettings_position': 'split',
'font_color': '#d4d4d4',
'font_color_light': rgb_to_hex(71, 71, 71),
'waves_color': rgb_to_hex(94, 26, 145),
'bg_gradiant_color': rgb_to_hex(101, 0, 94),
'accent_color': '#37b9dd',
'secondary_accent_color': '#b67ee1',
'card_height': '8',
'card_width': '16',
'error_popup': True,
'disable_image_browser': True,
'disable_waves_and_gradiant': False,
'server_default_port': 3333,
'auto_search_port': True,
'auto_start_server': 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,
'carret_style': 'thin',
'save_last_prompt_local_storage': True,
'color_mode': 'dark',
'webui': 'unknown'
}
def reset_settings():
save_settings(
get_default_settings())
from scripts.cozy_lib import tools
from scripts.cozy_lib.CozyLogger import CozyLogger
from scripts.cozy_lib.CozyNestConfig import CozyNestConfig
from scripts.cozy_lib.cozynest_image_browser import start_server
from scripts.cozy_lib.tools import output_folder_array
def request_restart():
@ -216,26 +83,6 @@ def serv_img_browser_socket(server_port=3333, auto_search_port=True, cnib_output
print(e)
def output_folder_array():
outdir_txt2img_samples = shared.opts.data['outdir_txt2img_samples']
outdir_img2img_samples = shared.opts.data['outdir_img2img_samples']
outdir_extras_samples = shared.opts.data['outdir_extras_samples']
base_dir = scripts.basedir()
# check if outdir_txt2img_samples is a relative path
if not os.path.isabs(outdir_txt2img_samples):
outdir_txt2img_samples = os.path.normpath(os.path.join(base_dir, outdir_txt2img_samples))
if not os.path.isabs(outdir_img2img_samples):
outdir_img2img_samples = os.path.normpath(os.path.join(base_dir, outdir_img2img_samples))
if not os.path.isabs(outdir_extras_samples):
outdir_extras_samples = os.path.normpath(os.path.join(base_dir, outdir_extras_samples))
images_folders = [
outdir_txt2img_samples,
outdir_img2img_samples,
outdir_extras_samples,
]
return images_folders
def start_server_in_dedicated_process(_images_folders, server_port):
def run_server():
asyncio.run(start_server(_images_folders, server_port, stopper))
@ -284,38 +131,12 @@ _server_port = None
def on_ui_tabs():
global _server_port
# shared options
config = get_dict_from_config()
# merge default settings with user settings
config = {**get_default_settings(), **config}
if config['webui'] == 'unknown' and hasattr(shared, 'get_version'):
version = shared.get_version()
# check if the 'app' is 'sd.next'
if version['app'] == 'sd.next':
config['webui'] = 'sd.next'
config['fetch_output_folder_from_a1111_settings'] = False
else:
config['webui'] = 'auto1111'
save_settings(config)
config = CozyNestConfig()
config.init()
config.migrate()
if config['webui'] == 'sd.next':
config['fetch_output_folder_from_a1111_settings'] = False
# check if cnib_output_folder is empty and/or need to be fetched from a1111 settings
cnib_output_folder = config.get('cnib_output_folder')
is_empty = cnib_output_folder == []
if not cnib_output_folder or is_empty:
cnib_output_folder = []
if config.get('fetch_output_folder_from_a1111_settings'):
# merge cnib_output_folder output_folder_array()
cnib_output_folder = cnib_output_folder + list(set(output_folder_array()) - set(cnib_output_folder))
config['cnib_output_folder'] = cnib_output_folder
# save the merged settings
save_settings(config)
CozyLogger.info(f"version: {config.get('version')}")
# check if the user has disabled the image browser
disable_image_browser_value = config.get('disable_image_browser')
@ -346,12 +167,12 @@ 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)
_new_img_data = tools.get_exif(path)
tools.new_image(_new_img_data)
asyncio.run(send_to_socket({
'what': 'image_saved',
'data': data,
'data': _new_img_data,
}, server_port))
if not disable_image_browser_value:
@ -389,7 +210,7 @@ async def send_to_socket(data, server_port):
await websocket.close()
break
except websockets.exceptions.ConnectionClosed:
except Exception:
pass
@ -405,19 +226,16 @@ def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
# Access POST parameters
data = await request.json()
# shared options
config = get_dict_from_config()
# merge default settings with user settings
config = {**get_default_settings(), **config,
**data}
config = CozyNestConfig()
save_settings(config)
config.save_settings(data)
return {"message": "Config saved successfully"}
@app.delete("/cozy-nest/config")
async def delete_config():
reset_settings()
config = CozyNestConfig()
config.reset_settings()
return {"message": "Config deleted successfully"}
@app.get("/cozy-nest/reloadui")
@ -457,7 +275,7 @@ def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
async def delete_index():
global _server_port
config = get_dict_from_config()
config = CozyNestConfig()
cnib_output_folder = config.get('cnib_output_folder')
if cnib_output_folder and cnib_output_folder != "":
tools.delete_index()
@ -487,7 +305,7 @@ def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
# do nothing for now
return Response(status_code=501, content="unimplemented")
config = get_dict_from_config()
config = CozyNestConfig()
archive_path = config.get('archive_path')
if not archive_path or archive_path == "":
# return {"message": "archive path not set"}
@ -536,6 +354,13 @@ def cozy_nest_api(_: Any, app: FastAPI, **kwargs):
pass
def init_extra_networks(_: Any, app: FastAPI, **kwargs):
from scripts.cozy_lib.cozy_extra_network import CozyExtraNetworksClass
CozyExtraNetworks = CozyExtraNetworksClass()
CozyExtraNetworks.create_api_route(app)
script_callbacks.on_ui_tabs(on_ui_tabs)
script_callbacks.on_app_started(cozy_nest_api)
script_callbacks.on_app_started(init_extra_networks)

View File

@ -1,138 +0,0 @@
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") or file.endswith(".jpg") or file.endswith(".jpeg"):
# 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.3.4"
"version": "2.4.0"
}