forked from extern/easydiffusion
commit
b2ab3f987c
27
CHANGES.md
Normal file
27
CHANGES.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# What's new?
|
||||||
|
|
||||||
|
## v2.4
|
||||||
|
### Major Changes
|
||||||
|
- **Support for custom VAE models**. You can place your VAE files in the `models/vae` folder, and refresh the browser page to use them. More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder
|
||||||
|
- **Experimental support for multiple GPUs!** It should work automatically. Just open one browser tab per GPU, and spread your tasks across your GPUs. For e.g. open our UI in two browser tabs if you have two GPUs. You can customize which GPUs it should use in the "Settings" tab, otherwise let it automatically pick the best GPUs. Thanks @madrang
|
||||||
|
- **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller
|
||||||
|
- **Progress bar.** Thanks @mdiller
|
||||||
|
- **Custom Image Modifiers** - You can now save your custom image modifiers! Your saved modifiers can include special characters like `{}, (), [], |`
|
||||||
|
- Drag and Drop **text files generated from previously saved images**, and copy settings to clipboard. Thanks @madrang
|
||||||
|
- Paste settings from clipboard. Thanks @JeLuf
|
||||||
|
- Bug fixes to reduce the chances of tasks crashing during long multi-hour runs (chrome can put long-running background tabs to sleep). Thanks @JeLuf and @madrang
|
||||||
|
- **Improved documentation.** Thanks @JeLuf and @jsuelwald
|
||||||
|
- Improved the codebase for dealing with system settings and UI settings. Thanks @mdiller
|
||||||
|
- Help instructions next to some setttings, and in the tab
|
||||||
|
- Show system info in the settings tab
|
||||||
|
- Keyboard shortcut: Ctrl+Enter to start a task
|
||||||
|
- Configuration to prevent the browser from opening on startup
|
||||||
|
- Lots of minor bug fixes
|
||||||
|
- A `What's New?` tab in the UI
|
||||||
|
|
||||||
|
### Detailed changelog
|
||||||
|
* 2.4.7 - 17 Nov 2022 - Fix a bug where Face Correction (GFPGAN) would fail on cuda:N (i.e. GPUs other than cuda:0), as well as fail on CPU if the system had an incompatible GPU.
|
||||||
|
* 2.4.6 - 16 Nov 2022 - Fix a regression in VRAM usage during startup, which caused 'Out of Memory' errors when starting on GPUs with 4gb (or less) VRAM
|
||||||
|
* 2.4.5 - 16 Nov 2022 - Add checkbox for "Open browser on startup".
|
||||||
|
* 2.4.5 - 16 Nov 2022 - Add a directory for core plugins that ship with Stable Diffusion UI by default.
|
||||||
|
* 2.4.5 - 16 Nov 2022 - Add a "What's New?" tab as a core plugin, which fetches the contents of CHANGES.md from the app's release branch.
|
@ -40,6 +40,7 @@ or for Windows
|
|||||||
`mklink /J \projects\stable-diffusion-ui-archive\ui \projects\stable-diffusion-ui-repo\ui` (link name first, source repo dir second)
|
`mklink /J \projects\stable-diffusion-ui-archive\ui \projects\stable-diffusion-ui-repo\ui` (link name first, source repo dir second)
|
||||||
9) Run the project again (like in step 2) and ensure you can still use the UI.
|
9) Run the project again (like in step 2) and ensure you can still use the UI.
|
||||||
10) Congrats, now any changes you make in your repo `ui` folder are linked to this running archive of the app and can be previewed in the browser.
|
10) Congrats, now any changes you make in your repo `ui` folder are linked to this running archive of the app and can be previewed in the browser.
|
||||||
|
11) Please update CHANGES.md in your pull requests.
|
||||||
|
|
||||||
Check the `ui/frontend/build/README.md` for instructions on running and building the React code.
|
Check the `ui/frontend/build/README.md` for instructions on running and building the React code.
|
||||||
|
|
||||||
|
@ -7,19 +7,20 @@
|
|||||||
<link rel="icon" type="image/png" href="/media/images/favicon-32x32.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="/media/images/favicon-32x32.png" sizes="32x32">
|
||||||
<link rel="stylesheet" href="/media/css/fonts.css?v=1">
|
<link rel="stylesheet" href="/media/css/fonts.css?v=1">
|
||||||
<link rel="stylesheet" href="/media/css/themes.css?v=3">
|
<link rel="stylesheet" href="/media/css/themes.css?v=3">
|
||||||
<link rel="stylesheet" href="/media/css/main.css?v=17">
|
<link rel="stylesheet" href="/media/css/main.css?v=19">
|
||||||
<link rel="stylesheet" href="/media/css/auto-save.css?v=5">
|
<link rel="stylesheet" href="/media/css/auto-save.css?v=5">
|
||||||
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css?v=4">
|
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css?v=4">
|
||||||
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css?v=1">
|
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css?v=1">
|
||||||
<link rel="stylesheet" href="/media/css/drawingboard.min.css">
|
<link rel="stylesheet" href="/media/css/drawingboard.min.css">
|
||||||
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
||||||
<script src="/media/js/drawingboard.min.js"></script>
|
<script src="/media/js/drawingboard.min.js"></script>
|
||||||
|
<script src="/media/js/marked.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="top-nav">
|
<div id="top-nav">
|
||||||
<div id="logo">
|
<div id="logo">
|
||||||
<h1>Stable Diffusion UI <small>v2.4.6 <span id="updateBranchLabel"></span></small></h1>
|
<h1>Stable Diffusion UI <small>v2.4.7 <span id="updateBranchLabel"></span></small></h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="server-status">
|
<div id="server-status">
|
||||||
<div id="server-status-color">●</div>
|
<div id="server-status-color">●</div>
|
||||||
@ -327,15 +328,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="media/js/parameters.js?v=9"></script>
|
|
||||||
<script src="media/js/plugins.js?v=1"></script>
|
|
||||||
<script src="media/js/utils.js?v=6"></script>
|
<script src="media/js/utils.js?v=6"></script>
|
||||||
|
<script src="media/js/parameters.js?v=10"></script>
|
||||||
|
<script src="media/js/plugins.js?v=1"></script>
|
||||||
<script src="media/js/inpainting-editor.js?v=1"></script>
|
<script src="media/js/inpainting-editor.js?v=1"></script>
|
||||||
<script src="media/js/image-modifiers.js?v=6"></script>
|
<script src="media/js/image-modifiers.js?v=6"></script>
|
||||||
<script src="media/js/auto-save.js?v=8"></script>
|
<script src="media/js/auto-save.js?v=8"></script>
|
||||||
<script src="media/js/main.js?v=22.1"></script>
|
<script src="media/js/main.js?v=23"></script>
|
||||||
<script src="media/js/themes.js?v=4"></script>
|
<script src="media/js/themes.js?v=4"></script>
|
||||||
<script src="media/js/dnd.js?v=9"></script>
|
<script src="media/js/dnd.js?v=10"></script>
|
||||||
<script>
|
<script>
|
||||||
async function init() {
|
async function init() {
|
||||||
await initSettings()
|
await initSettings()
|
||||||
|
@ -22,6 +22,11 @@ a:visited {
|
|||||||
label {
|
label {
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
}
|
}
|
||||||
|
code {
|
||||||
|
background: var(--background-color4);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
#prompt {
|
#prompt {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 65pt;
|
height: 65pt;
|
||||||
@ -898,6 +903,9 @@ input::file-selector-button {
|
|||||||
i.active {
|
i.active {
|
||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
.primaryButton.active {
|
||||||
|
background: hsl(var(--accent-hue), 100%, 50%);
|
||||||
|
}
|
||||||
#system-info {
|
#system-info {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
|
@ -432,8 +432,8 @@ function checkWriteToClipboardPermission (result) {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Add css class 'active'
|
// Add css class 'active'
|
||||||
copyIcon.classList.add('active')
|
copyIcon.classList.add('active')
|
||||||
// In 1000 ms remove the 'active' class
|
// In 350 ms remove the 'active' class
|
||||||
asyncDelay(1000).then(() => copyIcon.classList.remove('active'))
|
asyncDelay(350).then(() => copyIcon.classList.remove('active'))
|
||||||
const uiState = readUI()
|
const uiState = readUI()
|
||||||
TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key])
|
TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key])
|
||||||
if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) {
|
if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) {
|
||||||
@ -452,8 +452,8 @@ function checkWriteToClipboardPermission (result) {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Add css class 'active'
|
// Add css class 'active'
|
||||||
pasteIcon.classList.add('active')
|
pasteIcon.classList.add('active')
|
||||||
// In 1000 ms remove the 'active' class
|
// In 350 ms remove the 'active' class
|
||||||
asyncDelay(1000).then(() => pasteIcon.classList.remove('active'))
|
asyncDelay(350).then(() => pasteIcon.classList.remove('active'))
|
||||||
pasteFromClipboard()
|
pasteFromClipboard()
|
||||||
})
|
})
|
||||||
resetSettings.parentNode.insertBefore(pasteIcon, resetSettings)
|
resetSettings.parentNode.insertBefore(pasteIcon, resetSettings)
|
||||||
|
@ -1284,7 +1284,7 @@ document.querySelectorAll('.popup').forEach(popup => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
var tabElements = [];
|
var tabElements = [];
|
||||||
document.querySelectorAll(".tab").forEach(tab => {
|
function linkTabContents(tab) {
|
||||||
var name = tab.id.replace("tab-", "");
|
var name = tab.id.replace("tab-", "");
|
||||||
var content = document.getElementById(`tab-content-${name}`)
|
var content = document.getElementById(`tab-content-${name}`)
|
||||||
tabElements.push({
|
tabElements.push({
|
||||||
@ -1305,7 +1305,9 @@ document.querySelectorAll(".tab").forEach(tab => {
|
|||||||
content.classList.toggle("active")
|
content.classList.toggle("active")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(".tab").forEach(linkTabContents)
|
||||||
|
|
||||||
window.addEventListener("beforeunload", function(e) {
|
window.addEventListener("beforeunload", function(e) {
|
||||||
const msg = "Unsaved pictures will be lost!";
|
const msg = "Unsaved pictures will be lost!";
|
||||||
|
6
ui/media/js/marked.min.js
vendored
Normal file
6
ui/media/js/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -327,4 +327,7 @@ saveSettingsBtn.addEventListener('click', function() {
|
|||||||
'update_branch': updateBranch,
|
'update_branch': updateBranch,
|
||||||
'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked
|
'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked
|
||||||
})
|
})
|
||||||
|
|
||||||
|
saveSettingsBtn.classList.add('active')
|
||||||
|
asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active'))
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
Custom plugins in this folder will be shipped to all the users by default.
|
||||||
|
|
||||||
|
This allows UI features to be built as plugins (testing our Plugins API, and keeping our core lean and modular).
|
47
ui/plugins/ui/release-notes.plugin.js
Normal file
47
ui/plugins/ui/release-notes.plugin.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
(function() {
|
||||||
|
document.querySelector('#tab-container').insertAdjacentHTML('beforeend', `
|
||||||
|
<span id="tab-news" class="tab">
|
||||||
|
<span><i class="fa fa-bolt icon"></i> What's new?</span>
|
||||||
|
</span>
|
||||||
|
`)
|
||||||
|
|
||||||
|
document.querySelector('#tab-content-wrapper').insertAdjacentHTML('beforeend', `
|
||||||
|
<div id="tab-content-news" class="tab-content">
|
||||||
|
<div id="news" class="tab-content-inner">
|
||||||
|
Loading..
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
|
||||||
|
document.querySelector('body').insertAdjacentHTML('beforeend', `
|
||||||
|
<style>
|
||||||
|
#tab-content-news .tab-content-inner {
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10pt;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`)
|
||||||
|
|
||||||
|
linkTabContents(document.querySelector('#tab-news'))
|
||||||
|
|
||||||
|
let markedScript = document.createElement('script')
|
||||||
|
markedScript.src = '/media/js/marked.min.js'
|
||||||
|
|
||||||
|
markedScript.onload = async function() {
|
||||||
|
let appConfig = await fetch('/get/app_config')
|
||||||
|
appConfig = await appConfig.json()
|
||||||
|
|
||||||
|
let updateBranch = appConfig.update_branch || 'main'
|
||||||
|
|
||||||
|
let news = document.querySelector('#news')
|
||||||
|
let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`)
|
||||||
|
if (releaseNotes.status != 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
releaseNotes = await releaseNotes.text()
|
||||||
|
news.innerHTML = marked.parse(releaseNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('body').appendChild(markedScript)
|
||||||
|
})()
|
@ -236,9 +236,14 @@ def wait_model_move_to(model, target_device): # Send to target_device and wait u
|
|||||||
|
|
||||||
def load_model_gfpgan():
|
def load_model_gfpgan():
|
||||||
if thread_data.gfpgan_file is None: raise ValueError(f'Thread gfpgan_file is undefined.')
|
if thread_data.gfpgan_file is None: raise ValueError(f'Thread gfpgan_file is undefined.')
|
||||||
|
|
||||||
|
# hack for a bug in facexlib: https://github.com/xinntao/facexlib/pull/19/files
|
||||||
|
from facexlib.detection import retinaface
|
||||||
|
retinaface.device = torch.device(thread_data.device)
|
||||||
|
print('forced retinaface.device to', thread_data.device)
|
||||||
|
|
||||||
model_path = thread_data.gfpgan_file + ".pth"
|
model_path = thread_data.gfpgan_file + ".pth"
|
||||||
device = 'cuda:0' if force_gfpgan_to_cuda0 else thread_data.device
|
thread_data.model_gfpgan = GFPGANer(device=torch.device(thread_data.device), model_path=model_path, upscale=1, arch='clean', channel_multiplier=2, bg_upsampler=None)
|
||||||
thread_data.model_gfpgan = GFPGANer(device=torch.device(device), model_path=model_path, upscale=1, arch='clean', channel_multiplier=2, bg_upsampler=None)
|
|
||||||
print('loaded', thread_data.gfpgan_file, 'to', thread_data.model_gfpgan.device, 'precision', thread_data.precision)
|
print('loaded', thread_data.gfpgan_file, 'to', thread_data.model_gfpgan.device, 'precision', thread_data.precision)
|
||||||
|
|
||||||
def load_model_real_esrgan():
|
def load_model_real_esrgan():
|
||||||
@ -288,10 +293,10 @@ def apply_filters(filter_name, image_data, model_path=None):
|
|||||||
print(f'Applying filter {filter_name}...')
|
print(f'Applying filter {filter_name}...')
|
||||||
gc() # Free space before loading new data.
|
gc() # Free space before loading new data.
|
||||||
|
|
||||||
if filter_name == 'gfpgan':
|
|
||||||
if isinstance(image_data, torch.Tensor):
|
if isinstance(image_data, torch.Tensor):
|
||||||
image_data.to('cuda:0' if force_gfpgan_to_cuda0 else thread_data.device)
|
image_data.to(thread_data.device)
|
||||||
|
|
||||||
|
if filter_name == 'gfpgan':
|
||||||
if model_path is not None and model_path != thread_data.gfpgan_file:
|
if model_path is not None and model_path != thread_data.gfpgan_file:
|
||||||
thread_data.gfpgan_file = model_path
|
thread_data.gfpgan_file = model_path
|
||||||
load_model_gfpgan()
|
load_model_gfpgan()
|
||||||
@ -303,9 +308,6 @@ def apply_filters(filter_name, image_data, model_path=None):
|
|||||||
image_data = output[:,:,::-1]
|
image_data = output[:,:,::-1]
|
||||||
|
|
||||||
if filter_name == 'real_esrgan':
|
if filter_name == 'real_esrgan':
|
||||||
if isinstance(image_data, torch.Tensor):
|
|
||||||
image_data.to(thread_data.device)
|
|
||||||
|
|
||||||
if model_path is not None and model_path != thread_data.real_esrgan_file:
|
if model_path is not None and model_path != thread_data.real_esrgan_file:
|
||||||
thread_data.real_esrgan_file = model_path
|
thread_data.real_esrgan_file = model_path
|
||||||
load_model_real_esrgan()
|
load_model_real_esrgan()
|
||||||
|
@ -217,10 +217,6 @@ def thread_get_next_task():
|
|||||||
task = None
|
task = None
|
||||||
try: # Select a render task.
|
try: # Select a render task.
|
||||||
for queued_task in tasks_queue:
|
for queued_task in tasks_queue:
|
||||||
if queued_task.request.use_face_correction and runtime.thread_data.device == 'cpu' and is_alive() == 1:
|
|
||||||
queued_task.error = Exception('The CPU cannot be used to run this task currently. Please remove "Fix incorrect faces" from Image Settings and try again.')
|
|
||||||
task = queued_task
|
|
||||||
break
|
|
||||||
if queued_task.render_device and runtime.thread_data.device != queued_task.render_device:
|
if queued_task.render_device and runtime.thread_data.device != queued_task.render_device:
|
||||||
# Is asking for a specific render device.
|
# Is asking for a specific render device.
|
||||||
if is_alive(queued_task.render_device) > 0:
|
if is_alive(queued_task.render_device) > 0:
|
||||||
|
16
ui/server.py
16
ui/server.py
@ -16,7 +16,10 @@ sys.path.append(os.path.dirname(SD_UI_DIR))
|
|||||||
|
|
||||||
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, '..', 'scripts'))
|
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, '..', 'scripts'))
|
||||||
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'models'))
|
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'models'))
|
||||||
UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'plugins', 'ui'))
|
|
||||||
|
USER_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'plugins', 'ui'))
|
||||||
|
CORE_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, 'plugins', 'ui'))
|
||||||
|
UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, 'core'), (USER_UI_PLUGINS_DIR, 'user'))
|
||||||
|
|
||||||
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
|
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
|
||||||
TASK_TTL = 15 * 60 # Discard last session's task timeout
|
TASK_TTL = 15 * 60 # Discard last session's task timeout
|
||||||
@ -49,7 +52,7 @@ app = FastAPI()
|
|||||||
modifiers_cache = None
|
modifiers_cache = None
|
||||||
outpath = os.path.join(os.path.expanduser("~"), OUTPUT_DIRNAME)
|
outpath = os.path.join(os.path.expanduser("~"), OUTPUT_DIRNAME)
|
||||||
|
|
||||||
os.makedirs(UI_PLUGINS_DIR, exist_ok=True)
|
os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True)
|
||||||
|
|
||||||
# don't show access log entries for URLs that start with the given prefix
|
# don't show access log entries for URLs that start with the given prefix
|
||||||
ACCESS_LOG_SUPPRESS_PATH_PREFIXES = ['/ping', '/image', '/modifier-thumbnails']
|
ACCESS_LOG_SUPPRESS_PATH_PREFIXES = ['/ping', '/image', '/modifier-thumbnails']
|
||||||
@ -57,7 +60,9 @@ ACCESS_LOG_SUPPRESS_PATH_PREFIXES = ['/ping', '/image', '/modifier-thumbnails']
|
|||||||
NOCACHE_HEADERS={"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
|
NOCACHE_HEADERS={"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
|
||||||
|
|
||||||
app.mount('/media', StaticFiles(directory=os.path.join(SD_UI_DIR, 'media')), name="media")
|
app.mount('/media', StaticFiles(directory=os.path.join(SD_UI_DIR, 'media')), name="media")
|
||||||
app.mount('/plugins', StaticFiles(directory=UI_PLUGINS_DIR), name="plugins")
|
|
||||||
|
for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES:
|
||||||
|
app.mount(f'/plugins/{dir_prefix}', StaticFiles(directory=plugins_dir), name=f"plugins-{dir_prefix}")
|
||||||
|
|
||||||
def getConfig(default_val=APP_CONFIG_DEFAULTS):
|
def getConfig(default_val=APP_CONFIG_DEFAULTS):
|
||||||
try:
|
try:
|
||||||
@ -223,9 +228,10 @@ def getModels():
|
|||||||
def getUIPlugins():
|
def getUIPlugins():
|
||||||
plugins = []
|
plugins = []
|
||||||
|
|
||||||
for file in os.listdir(UI_PLUGINS_DIR):
|
for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES:
|
||||||
|
for file in os.listdir(plugins_dir):
|
||||||
if file.endswith('.plugin.js'):
|
if file.endswith('.plugin.js'):
|
||||||
plugins.append(f'/plugins/{file}')
|
plugins.append(f'/plugins/{dir_prefix}/{file}')
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user