ui_extensions.py 12.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
import json
import os.path
import sys
import time
import traceback

import git

import gradio as gr
import html
11
import shutil
12 13

from modules import extensions, shared, paths
14
from modules.call_queue import wrap_gradio_gpu_call
15

A
AUTOMATIC 已提交
16 17 18
available_extensions = {"extensions": []}


19
def check_access():
J
jcowens 已提交
20
    assert not shared.cmd_opts.disable_extension_access, "extension access disabled because of command line flags"
21 22


23
def apply_and_restart(disable_list, update_list):
24 25
    check_access()

26 27 28 29 30 31 32 33 34 35 36 37 38
    disabled = json.loads(disable_list)
    assert type(disabled) == list, f"wrong disable_list data for apply_and_restart: {disable_list}"

    update = json.loads(update_list)
    assert type(update) == list, f"wrong update_list data for apply_and_restart: {update_list}"

    update = set(update)

    for ext in extensions.extensions:
        if ext.name not in update:
            continue

        try:
39
            ext.fetch_and_reset_hard()
40
        except Exception:
41
            print(f"Error getting updates for {ext.name}:", file=sys.stderr)
42 43 44 45 46 47 48 49 50
            print(traceback.format_exc(), file=sys.stderr)

    shared.opts.disabled_extensions = disabled
    shared.opts.save(shared.config_filename)

    shared.state.interrupt()
    shared.state.need_restart = True


51
def check_updates(id_task, disable_list):
52 53
    check_access()

54 55 56 57 58 59 60 61
    disabled = json.loads(disable_list)
    assert type(disabled) == list, f"wrong disable_list data for apply_and_restart: {disable_list}"

    exts = [ext for ext in extensions.extensions if ext.remote is not None and ext.name not in disabled]
    shared.state.job_count = len(exts)

    for ext in exts:
        shared.state.textinfo = ext.name
62 63 64 65 66 67 68

        try:
            ext.check_updates()
        except Exception:
            print(f"Error checking updates for {ext.name}:", file=sys.stderr)
            print(traceback.format_exc(), file=sys.stderr)

69 70 71
        shared.state.nextjob()

    return extension_table(), ""
72 73 74 75 76 77 78 79 80


def extension_table():
    code = f"""<!-- {time.time()} -->
    <table id="extensions">
        <thead>
            <tr>
                <th><abbr title="Use checkbox to enable the extension; it will be enabled or disabled when you click apply button">Extension</abbr></th>
                <th>URL</th>
81
                <th><abbr title="Extension version">Version</abbr></th>
82 83 84 85 86 87 88
                <th><abbr title="Use checkbox to mark the extension for update; it will be updated when you click apply button">Update</abbr></th>
            </tr>
        </thead>
        <tbody>
    """

    for ext in extensions.extensions:
89
        remote = f"""<a href="{html.escape(ext.remote or '')}" target="_blank">{html.escape("built-in" if ext.is_builtin else ext.remote or '')}</a>"""
A
AUTOMATIC 已提交
90

91 92 93 94 95 96 97 98
        if ext.can_update:
            ext_status = f"""<label><input class="gr-check-radio gr-checkbox" name="update_{html.escape(ext.name)}" checked="checked" type="checkbox">{html.escape(ext.status)}</label>"""
        else:
            ext_status = ext.status

        code += f"""
            <tr>
                <td><label><input class="gr-check-radio gr-checkbox" name="enable_{html.escape(ext.name)}" type="checkbox" {'checked="checked"' if ext.enabled else ''}>{html.escape(ext.name)}</label></td>
A
AUTOMATIC 已提交
99
                <td>{remote}</td>
100
                <td>{ext.version}</td>
101 102 103 104 105 106 107 108 109 110 111 112
                <td{' class="extension_status"' if ext.remote is not None else ''}>{ext_status}</td>
            </tr>
    """

    code += """
        </tbody>
    </table>
    """

    return code


A
AUTOMATIC 已提交
113 114 115 116 117 118 119 120
def normalize_git_url(url):
    if url is None:
        return ""

    url = url.replace(".git", "")
    return url


121
def install_extension_from_url(dirname, url):
122 123
    check_access()

124 125 126 127
    assert url, 'No URL specified'

    if dirname is None or dirname == "":
        *parts, last_part = url.split('/')
A
AUTOMATIC 已提交
128
        last_part = normalize_git_url(last_part)
129 130 131 132 133 134

        dirname = last_part

    target_dir = os.path.join(extensions.extensions_dir, dirname)
    assert not os.path.exists(target_dir), f'Extension directory already exists: {target_dir}'

A
AUTOMATIC 已提交
135 136
    normalized_url = normalize_git_url(url)
    assert len([x for x in extensions.extensions if normalize_git_url(x.remote) == normalized_url]) == 0, 'Extension with this URL is already installed'
137

138
    tmpdir = os.path.join(paths.data_path, "tmp", dirname)
139 140 141

    try:
        shutil.rmtree(tmpdir, True)
F
Ftps 已提交
142 143 144 145 146
        with git.Repo.clone_from(url, tmpdir) as repo:
            repo.remote().fetch()
            for submodule in repo.submodules:
                submodule.update()
        os.rename(tmpdir, target_dir)
147

148 149 150
        import launch
        launch.run_extension_installer(target_dir)

151 152 153 154 155 156
        extensions.list_extensions()
        return [extension_table(), html.escape(f"Installed into {target_dir}. Use Installed tab to restart.")]
    finally:
        shutil.rmtree(tmpdir, True)


157
def install_extension_from_index(url, hide_tags, sort_column):
A
AUTOMATIC 已提交
158 159
    ext_table, message = install_extension_from_url(None, url)

160
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column)
A
AUTOMATIC 已提交
161

162
    return code, ext_table, message
A
AUTOMATIC 已提交
163

164

165
def refresh_available_extensions(url, hide_tags, sort_column):
A
AUTOMATIC 已提交
166 167 168 169 170 171 172 173
    global available_extensions

    import urllib.request
    with urllib.request.urlopen(url) as response:
        text = response.read()

    available_extensions = json.loads(text)

174
    code, tags = refresh_available_extensions_from_data(hide_tags, sort_column)
175 176 177 178

    return url, code, gr.CheckboxGroup.update(choices=tags), ''


179 180
def refresh_available_extensions_for_tags(hide_tags, sort_column):
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column)
A
AUTOMATIC 已提交
181

182
    return code, ''
A
AUTOMATIC 已提交
183

184

185 186 187 188 189 190 191 192 193 194 195
sort_ordering = [
    # (reverse, order_by_function)
    (True, lambda x: x.get('added', 'z')),
    (False, lambda x: x.get('added', 'z')),
    (False, lambda x: x.get('name', 'z')),
    (True, lambda x: x.get('name', 'z')),
    (False, lambda x: 'z'),
]


def refresh_available_extensions_from_data(hide_tags, sort_column):
A
AUTOMATIC 已提交
196 197 198
    extlist = available_extensions["extensions"]
    installed_extension_urls = {normalize_git_url(extension.remote): extension.name for extension in extensions.extensions}

199 200 201 202
    tags = available_extensions.get("tags", {})
    tags_to_hide = set(hide_tags)
    hidden = 0

A
AUTOMATIC 已提交
203 204 205 206 207 208 209 210 211 212 213 214
    code = f"""<!-- {time.time()} -->
    <table id="available_extensions">
        <thead>
            <tr>
                <th>Extension</th>
                <th>Description</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
    """

215 216 217
    sort_reverse, sort_function = sort_ordering[sort_column if 0 <= sort_column < len(sort_ordering) else 0]

    for ext in sorted(extlist, key=sort_function, reverse=sort_reverse):
A
AUTOMATIC 已提交
218
        name = ext.get("name", "noname")
219
        added = ext.get('added', 'unknown')
A
AUTOMATIC 已提交
220 221
        url = ext.get("url", None)
        description = ext.get("description", "")
222
        extension_tags = ext.get("tags", [])
A
AUTOMATIC 已提交
223 224 225 226

        if url is None:
            continue

A
AUTOMATIC 已提交
227 228 229
        existing = installed_extension_urls.get(normalize_git_url(url), None)
        extension_tags = extension_tags + ["installed"] if existing else extension_tags

230 231 232 233
        if len([x for x in extension_tags if x in tags_to_hide]) > 0:
            hidden += 1
            continue

A
AUTOMATIC 已提交
234 235
        install_code = f"""<input onclick="install_extension_from_index(this, '{html.escape(url)}')" type="button" value="{"Install" if not existing else "Installed"}" {"disabled=disabled" if existing else ""} class="gr-button gr-button-lg gr-button-secondary">"""

236 237
        tags_text = ", ".join([f"<span class='extension-tag' title='{tags.get(x, '')}'>{x}</span>" for x in extension_tags])

A
AUTOMATIC 已提交
238 239
        code += f"""
            <tr>
240
                <td><a href="{html.escape(url)}" target="_blank">{html.escape(name)}</a><br />{tags_text}</td>
241
                <td>{html.escape(description)}<p class="info"><span class="date_added">Added: {html.escape(added)}</span></p></td>
A
AUTOMATIC 已提交
242 243
                <td>{install_code}</td>
            </tr>
A
AUTOMATIC 已提交
244 245 246 247 248
        
        """

        for tag in [x for x in extension_tags if x not in tags]:
            tags[tag] = tag
A
AUTOMATIC 已提交
249 250 251 252 253 254

    code += """
        </tbody>
    </table>
    """

255 256 257 258
    if hidden > 0:
        code += f"<p>Extension hidden: {hidden}</p>"

    return code, list(tags)
A
AUTOMATIC 已提交
259 260


261 262 263 264 265 266 267
def create_ui():
    import modules.ui

    with gr.Blocks(analytics_enabled=False) as ui:
        with gr.Tabs(elem_id="tabs_extensions") as tabs:
            with gr.TabItem("Installed"):

268
                with gr.Row(elem_id="extensions_installed_top"):
269 270
                    apply = gr.Button(value="Apply and restart UI", variant="primary")
                    check = gr.Button(value="Check for updates")
A
AUTOMATIC 已提交
271 272
                    extensions_disabled_list = gr.Text(elem_id="extensions_disabled_list", visible=False).style(container=False)
                    extensions_update_list = gr.Text(elem_id="extensions_update_list", visible=False).style(container=False)
273

274
                info = gr.HTML()
275 276 277 278 279 280 281 282 283 284
                extensions_table = gr.HTML(lambda: extension_table())

                apply.click(
                    fn=apply_and_restart,
                    _js="extensions_apply",
                    inputs=[extensions_disabled_list, extensions_update_list],
                    outputs=[],
                )

                check.click(
285
                    fn=wrap_gradio_gpu_call(check_updates, extra_outputs=[gr.update()]),
286
                    _js="extensions_check",
287 288
                    inputs=[info, extensions_disabled_list],
                    outputs=[extensions_table, info],
289 290
                )

A
AUTOMATIC 已提交
291 292 293
            with gr.TabItem("Available"):
                with gr.Row():
                    refresh_available_extensions_button = gr.Button(value="Load from:", variant="primary")
294
                    available_extensions_index = gr.Text(value="https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui-extensions/master/index.json", label="Extension index URL").style(container=False)
A
AUTOMATIC 已提交
295 296 297
                    extension_to_install = gr.Text(elem_id="extension_to_install", visible=False)
                    install_extension_button = gr.Button(elem_id="install_extension_button", visible=False)

298
                with gr.Row():
A
AUTOMATIC 已提交
299
                    hide_tags = gr.CheckboxGroup(value=["ads", "localization", "installed"], label="Hide extensions with tags", choices=["script", "ads", "localization", "installed"])
300
                    sort_column = gr.Radio(value="newest first", label="Order", choices=["newest first", "oldest first", "a-z", "z-a", "internal order", ], type="index")
301

A
AUTOMATIC 已提交
302 303 304 305
                install_result = gr.HTML()
                available_extensions_table = gr.HTML()

                refresh_available_extensions_button.click(
306
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update()]),
307
                    inputs=[available_extensions_index, hide_tags, sort_column],
308
                    outputs=[available_extensions_index, available_extensions_table, hide_tags, install_result],
A
AUTOMATIC 已提交
309 310 311 312
                )

                install_extension_button.click(
                    fn=modules.ui.wrap_gradio_call(install_extension_from_index, extra_outputs=[gr.update(), gr.update()]),
313
                    inputs=[extension_to_install, hide_tags, sort_column],
A
AUTOMATIC 已提交
314 315 316
                    outputs=[available_extensions_table, extensions_table, install_result],
                )

317 318
                hide_tags.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
319 320 321 322 323 324 325
                    inputs=[hide_tags, sort_column],
                    outputs=[available_extensions_table, install_result]
                )

                sort_column.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
                    inputs=[hide_tags, sort_column],
326 327 328
                    outputs=[available_extensions_table, install_result]
                )

329 330 331
            with gr.TabItem("Install from URL"):
                install_url = gr.Text(label="URL for extension's git repository")
                install_dirname = gr.Text(label="Local directory name", placeholder="Leave empty for auto")
A
AUTOMATIC 已提交
332 333
                install_button = gr.Button(value="Install", variant="primary")
                install_result = gr.HTML(elem_id="extension_install_result")
334

A
AUTOMATIC 已提交
335
                install_button.click(
336 337
                    fn=modules.ui.wrap_gradio_call(install_extension_from_url, extra_outputs=[gr.update()]),
                    inputs=[install_dirname, install_url],
A
AUTOMATIC 已提交
338
                    outputs=[extensions_table, install_result],
339 340 341
                )

    return ui