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

import git

import gradio as gr
import html
12 13
import shutil
import errno
14 15

from modules import extensions, shared, paths
16
from modules.call_queue import wrap_gradio_gpu_call
17

A
AUTOMATIC 已提交
18 19 20
available_extensions = {"extensions": []}


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


25
def apply_and_restart(disable_list, update_list):
26 27
    check_access()

28 29 30 31 32 33 34 35 36 37 38 39 40
    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:
41
            ext.fetch_and_reset_hard()
42
        except Exception:
43
            print(f"Error getting updates for {ext.name}:", file=sys.stderr)
44 45 46 47 48 49 50 51 52
            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


53
def check_updates(id_task, disable_list):
54 55
    check_access()

56 57 58 59 60 61 62 63
    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
64 65 66 67 68 69 70

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

71 72 73
        shared.state.nextjob()

    return extension_table(), ""
74 75 76 77 78 79 80 81 82


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>
83
                <th><abbr title="Extension version">Version</abbr></th>
84 85 86 87 88 89 90
                <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:
91
        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 已提交
92

93 94 95 96 97 98 99 100
        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 已提交
101
                <td>{remote}</td>
102
                <td>{ext.version}</td>
103 104 105 106 107 108 109 110 111 112 113 114
                <td{' class="extension_status"' if ext.remote is not None else ''}>{ext_status}</td>
            </tr>
    """

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

    return code


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

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


123
def install_extension_from_url(dirname, url):
124 125
    check_access()

126 127 128 129
    assert url, 'No URL specified'

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

        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 已提交
137 138
    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'
139

140
    tmpdir = os.path.join(paths.data_path, "tmp", dirname)
141 142 143 144 145 146 147

    try:
        shutil.rmtree(tmpdir, True)

        repo = git.Repo.clone_from(url, tmpdir)
        repo.remote().fetch()

148 149 150 151 152 153 154 155 156 157 158 159
        try:
            os.rename(tmpdir, target_dir)
        except OSError as err:
            # TODO what does this do on windows? I think it'll be a different error code but I don't have a system to check it
            # Shouldn't cause any new issues at least but we probably want to handle it there too.
            if err.errno == errno.EXDEV:
                # Cross device link, typical in docker or when tmp/ and extensions/ are on different file systems
                # Since we can't use a rename, do the slower but more versitile shutil.move()
                shutil.move(tmpdir, target_dir)
            else:
                # Something else, not enough free space, permissions, etc.  rethrow it so that it gets handled.
                raise(err)
160

161 162 163
        import launch
        launch.run_extension_installer(target_dir)

164 165 166 167 168 169
        extensions.list_extensions()
        return [extension_table(), html.escape(f"Installed into {target_dir}. Use Installed tab to restart.")]
    finally:
        shutil.rmtree(tmpdir, True)


170
def install_extension_from_index(url, hide_tags, sort_column):
A
AUTOMATIC 已提交
171 172
    ext_table, message = install_extension_from_url(None, url)

173
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column)
A
AUTOMATIC 已提交
174

175
    return code, ext_table, message
A
AUTOMATIC 已提交
176

177

178
def refresh_available_extensions(url, hide_tags, sort_column):
A
AUTOMATIC 已提交
179 180 181 182 183 184 185 186
    global available_extensions

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

    available_extensions = json.loads(text)

187
    code, tags = refresh_available_extensions_from_data(hide_tags, sort_column)
188 189 190 191

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


192 193
def refresh_available_extensions_for_tags(hide_tags, sort_column):
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column)
A
AUTOMATIC 已提交
194

195
    return code, ''
A
AUTOMATIC 已提交
196

197

198 199 200 201 202 203 204 205 206 207 208
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 已提交
209 210 211
    extlist = available_extensions["extensions"]
    installed_extension_urls = {normalize_git_url(extension.remote): extension.name for extension in extensions.extensions}

212 213 214 215
    tags = available_extensions.get("tags", {})
    tags_to_hide = set(hide_tags)
    hidden = 0

A
AUTOMATIC 已提交
216 217 218 219 220 221 222 223 224 225 226 227
    code = f"""<!-- {time.time()} -->
    <table id="available_extensions">
        <thead>
            <tr>
                <th>Extension</th>
                <th>Description</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
    """

228 229 230
    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 已提交
231
        name = ext.get("name", "noname")
232
        added = ext.get('added', 'unknown')
A
AUTOMATIC 已提交
233 234
        url = ext.get("url", None)
        description = ext.get("description", "")
235
        extension_tags = ext.get("tags", [])
A
AUTOMATIC 已提交
236 237 238 239

        if url is None:
            continue

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

243 244 245 246
        if len([x for x in extension_tags if x in tags_to_hide]) > 0:
            hidden += 1
            continue

A
AUTOMATIC 已提交
247 248
        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">"""

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

A
AUTOMATIC 已提交
251 252
        code += f"""
            <tr>
253
                <td><a href="{html.escape(url)}" target="_blank">{html.escape(name)}</a><br />{tags_text}</td>
254
                <td>{html.escape(description)}<p class="info"><span class="date_added">Added: {html.escape(added)}</span></p></td>
A
AUTOMATIC 已提交
255 256
                <td>{install_code}</td>
            </tr>
A
AUTOMATIC 已提交
257 258 259 260 261
        
        """

        for tag in [x for x in extension_tags if x not in tags]:
            tags[tag] = tag
A
AUTOMATIC 已提交
262 263 264 265 266 267

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

268 269 270 271
    if hidden > 0:
        code += f"<p>Extension hidden: {hidden}</p>"

    return code, list(tags)
A
AUTOMATIC 已提交
272 273


274 275 276 277 278 279 280
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"):

281
                with gr.Row(elem_id="extensions_installed_top"):
282 283
                    apply = gr.Button(value="Apply and restart UI", variant="primary")
                    check = gr.Button(value="Check for updates")
A
AUTOMATIC 已提交
284 285
                    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)
286

287
                info = gr.HTML()
288 289 290 291 292 293 294 295 296 297
                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(
298
                    fn=wrap_gradio_gpu_call(check_updates, extra_outputs=[gr.update()]),
299
                    _js="extensions_check",
300 301
                    inputs=[info, extensions_disabled_list],
                    outputs=[extensions_table, info],
302 303
                )

A
AUTOMATIC 已提交
304 305 306
            with gr.TabItem("Available"):
                with gr.Row():
                    refresh_available_extensions_button = gr.Button(value="Load from:", variant="primary")
307
                    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 已提交
308 309 310
                    extension_to_install = gr.Text(elem_id="extension_to_install", visible=False)
                    install_extension_button = gr.Button(elem_id="install_extension_button", visible=False)

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

A
AUTOMATIC 已提交
315 316 317 318
                install_result = gr.HTML()
                available_extensions_table = gr.HTML()

                refresh_available_extensions_button.click(
319
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update()]),
320
                    inputs=[available_extensions_index, hide_tags, sort_column],
321
                    outputs=[available_extensions_index, available_extensions_table, hide_tags, install_result],
A
AUTOMATIC 已提交
322 323 324 325
                )

                install_extension_button.click(
                    fn=modules.ui.wrap_gradio_call(install_extension_from_index, extra_outputs=[gr.update(), gr.update()]),
326
                    inputs=[extension_to_install, hide_tags, sort_column],
A
AUTOMATIC 已提交
327 328 329
                    outputs=[available_extensions_table, extensions_table, install_result],
                )

330 331
                hide_tags.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
332 333 334 335 336 337 338
                    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],
339 340 341
                    outputs=[available_extensions_table, install_result]
                )

342 343 344
            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 已提交
345 346
                install_button = gr.Button(value="Install", variant="primary")
                install_result = gr.HTML(elem_id="extension_install_result")
347

A
AUTOMATIC 已提交
348
                install_button.click(
349 350
                    fn=modules.ui.wrap_gradio_call(install_extension_from_url, extra_outputs=[gr.update()]),
                    inputs=[install_dirname, install_url],
A
AUTOMATIC 已提交
351
                    outputs=[extensions_table, install_result],
352 353 354
                )

    return ui