diff --git a/blender_kitsu/gazu/__init__.py b/blender_kitsu/gazu/__init__.py index 13c2d546..14ce8de4 100644 --- a/blender_kitsu/gazu/__init__.py +++ b/blender_kitsu/gazu/__init__.py @@ -6,6 +6,7 @@ from . import asset from . import casting from . import context from . import entity +from . import edit from . import files from . import project from . import person diff --git a/blender_kitsu/gazu/edit.py b/blender_kitsu/gazu/edit.py new file mode 100644 index 00000000..178f5464 --- /dev/null +++ b/blender_kitsu/gazu/edit.py @@ -0,0 +1,59 @@ +from blender_kitsu import gazu +from . import client as raw +from .sorting import sort_by_name + +from .cache import cache +from .helpers import normalize_model_parameter + +default = raw.default_client + +@cache +def get_all_edits(relations=False, client=default): + """ + Retrieve all edit entries. + """ + params = {} + if relations: + params = {"relations": "true"} + path = "edits/all" + edits = raw.fetch_all(path, params, client=client) + return sort_by_name(edits) + +@cache +def get_edit(edit_id, relations=False, client=default): + """ + Retrieve all edit entries. + """ + edit_entry = normalize_model_parameter(edit_id) + params = {} + if relations: + params = {"relations": "true"} + path = f"edits/{edit_entry['id']}" + edit_entry = raw.fetch_all(path, params, client=client) + return edit_entry + +@cache +def get_all_edits_with_tasks(relations=False, client=default): + """ + Retrieve all edit entries. + """ + params = {} + if relations: + params = {"relations": "true"} + path = "edits/with-tasks" + edits_with_tasks = raw.fetch_all(path, params, client=client) + return sort_by_name(edits_with_tasks) + +@cache +def get_all_previews_for_edit(edit, client=default): + """ + Args: + episode (str / dict): The episode dict or the episode ID. + + Returns: + list: Shots which are children of given episode. + """ + edit = normalize_model_parameter(edit) + edit_previews = (raw.fetch_all(f"edits/{edit['id']}/preview-files", client=client)) + for key in [key for key in enumerate(edit_previews.keys())]: + return edit_previews[key[1]] \ No newline at end of file diff --git a/blender_kitsu/gazu/entity.py b/blender_kitsu/gazu/entity.py index 8ac2cef1..32dcfcf1 100644 --- a/blender_kitsu/gazu/entity.py +++ b/blender_kitsu/gazu/entity.py @@ -104,3 +104,16 @@ def remove_entity(entity, force=False, client=default): if force: params = {"force": "true"} return raw.delete(path, params, client=client) + +def update_entity(entity, client=default): + """ + Save given shot data into the API. Metadata are fully replaced by the ones + set on given shot. + + Args: + Entity (dict): The shot dict to update. + + Returns: + dict: Updated entity. + """ + return raw.put(f"data/entities/{entity['id']}", entity, client=client) \ No newline at end of file diff --git a/blender_kitsu/gazu/task.py b/blender_kitsu/gazu/task.py index 98326e64..27121777 100644 --- a/blender_kitsu/gazu/task.py +++ b/blender_kitsu/gazu/task.py @@ -147,6 +147,20 @@ def all_tasks_for_episode(episode, relations=False, client=default): return sort_by_name(tasks) +@cache +def all_tasks_for_edit(edit, relations=False, client=default): + """ + Retrieve all tasks directly linked to given edit. + """ + edit = normalize_model_parameter(edit) + params = {} + if relations: + params = {"relations": "true"} + path = "edits/%s/tasks" % edit["id"] + tasks = raw.fetch_all(path, params, client=client) + return sort_by_name(tasks) + + @cache def all_shot_tasks_for_sequence(sequence, relations=False, client=default): """ diff --git a/blender_kitsu/playblast/core.py b/blender_kitsu/playblast/core.py new file mode 100644 index 00000000..d7c48594 --- /dev/null +++ b/blender_kitsu/playblast/core.py @@ -0,0 +1,219 @@ +import contextlib + +from blender_kitsu import ( + prefs, +) + +# TODO refactor so these are nest-able and re-use code + +@contextlib.contextmanager +def override_render_format(self, context): + """Overrides the render settings for playblast creation""" + rd = context.scene.render + # Format render settings. + percentage = rd.resolution_percentage + file_format = rd.image_settings.file_format + ffmpeg_constant_rate = rd.ffmpeg.constant_rate_factor + ffmpeg_codec = rd.ffmpeg.codec + ffmpeg_format = rd.ffmpeg.format + ffmpeg_audio_codec = rd.ffmpeg.audio_codec + + try: + rd.resolution_percentage = 100 + rd.image_settings.file_format = "FFMPEG" + rd.ffmpeg.constant_rate_factor = "HIGH" + rd.ffmpeg.codec = "H264" + rd.ffmpeg.format = "MPEG4" + rd.ffmpeg.audio_codec = "AAC" + + yield + + finally: + rd.resolution_percentage = percentage + rd.image_settings.file_format = file_format + rd.ffmpeg.codec = ffmpeg_codec + rd.ffmpeg.constant_rate_factor = ffmpeg_constant_rate + rd.ffmpeg.format = ffmpeg_format + rd.ffmpeg.audio_codec = ffmpeg_audio_codec + +@contextlib.contextmanager +def override_render_path(self, context, render_file_path): + """Overrides the render settings for playblast creation""" + rd = context.scene.render + # Filepath. + filepath = rd.filepath + + try: + # Filepath. + rd.filepath = render_file_path + + yield + + finally: + # Filepath. + rd.filepath = filepath + + +@contextlib.contextmanager +def override_render_settings(self, context, render_file_path): + """Overrides the render settings for playblast creation""" + addon_prefs = prefs.addon_prefs_get(context) + rd = context.scene.render + sps = context.space_data.shading + sp = context.space_data + # Get first last name for stamp note text. + session = prefs.session_get(context) + first_name = session.data.user["first_name"] + last_name = session.data.user["last_name"] + # Remember current render settings in order to restore them later. + + # Filepath. + filepath = rd.filepath + + # Format render settings. + percentage = rd.resolution_percentage + file_format = rd.image_settings.file_format + ffmpeg_constant_rate = rd.ffmpeg.constant_rate_factor + ffmpeg_codec = rd.ffmpeg.codec + ffmpeg_format = rd.ffmpeg.format + ffmpeg_audio_codec = rd.ffmpeg.audio_codec + + # Stamp metadata settings. + metadata_input = rd.metadata_input + use_stamp_date = rd.use_stamp_date + use_stamp_time = rd.use_stamp_time + use_stamp_render_time = rd.use_stamp_render_time + use_stamp_frame = rd.use_stamp_frame + use_stamp_frame_range = rd.use_stamp_frame_range + use_stamp_memory = rd.use_stamp_memory + use_stamp_hostname = rd.use_stamp_hostname + use_stamp_camera = rd.use_stamp_camera + use_stamp_lens = rd.use_stamp_lens + use_stamp_scene = rd.use_stamp_scene + use_stamp_marker = rd.use_stamp_marker + use_stamp_marker = rd.use_stamp_marker + use_stamp_note = rd.use_stamp_note + stamp_note_text = rd.stamp_note_text + use_stamp = rd.use_stamp + stamp_font_size = rd.stamp_font_size + stamp_foreground = rd.stamp_foreground + stamp_background = rd.stamp_background + use_stamp_labels = rd.use_stamp_labels + + # Space data settings. + shading_type = sps.type + shading_light = sps.light + studio_light = sps.studio_light + color_type = sps.color_type + background_type = sps.background_type + + show_backface_culling = sps.show_backface_culling + show_xray = sps.show_xray + show_shadows = sps.show_shadows + show_cavity = sps.show_cavity + show_object_outline = sps.show_object_outline + show_specular_highlight = sps.show_specular_highlight + + show_gizmo = sp.show_gizmo + + try: + # Filepath. + rd.filepath = render_file_path + + # Format render settings. + rd.resolution_percentage = 100 + rd.image_settings.file_format = "FFMPEG" + rd.ffmpeg.constant_rate_factor = "HIGH" + rd.ffmpeg.codec = "H264" + rd.ffmpeg.format = "MPEG4" + rd.ffmpeg.audio_codec = "AAC" + + # Stamp metadata settings. + rd.metadata_input = "SCENE" + rd.use_stamp_date = False + rd.use_stamp_time = False + rd.use_stamp_render_time = False + rd.use_stamp_frame = True + rd.use_stamp_frame_range = False + rd.use_stamp_memory = False + rd.use_stamp_hostname = False + rd.use_stamp_camera = False + rd.use_stamp_lens = True + rd.use_stamp_scene = False + rd.use_stamp_marker = False + rd.use_stamp_marker = False + rd.use_stamp_note = True + rd.stamp_note_text = f"Animator: {first_name} {last_name}" + rd.use_stamp = True + rd.stamp_font_size = 12 + rd.stamp_foreground = (0.8, 0.8, 0.8, 1) + rd.stamp_background = (0, 0, 0, 0.25) + rd.use_stamp_labels = True + + # Space data settings. + sps.type = "SOLID" + sps.light = "STUDIO" + sps.studio_light = "Default" + sps.color_type = "MATERIAL" + sps.background_type = "THEME" + + sps.show_backface_culling = False + sps.show_xray = False + sps.show_shadows = False + sps.show_cavity = False + sps.show_object_outline = False + sps.show_specular_highlight = True + + sp.show_gizmo = False + + yield + + finally: + # Filepath. + rd.filepath = filepath + + # Return the render settings to normal. + rd.resolution_percentage = percentage + rd.image_settings.file_format = file_format + rd.ffmpeg.codec = ffmpeg_codec + rd.ffmpeg.constant_rate_factor = ffmpeg_constant_rate + rd.ffmpeg.format = ffmpeg_format + rd.ffmpeg.audio_codec = ffmpeg_audio_codec + + # Stamp metadata settings. + rd.metadata_input = metadata_input + rd.use_stamp_date = use_stamp_date + rd.use_stamp_time = use_stamp_time + rd.use_stamp_render_time = use_stamp_render_time + rd.use_stamp_frame = use_stamp_frame + rd.use_stamp_frame_range = use_stamp_frame_range + rd.use_stamp_memory = use_stamp_memory + rd.use_stamp_hostname = use_stamp_hostname + rd.use_stamp_camera = use_stamp_camera + rd.use_stamp_lens = use_stamp_lens + rd.use_stamp_scene = use_stamp_scene + rd.use_stamp_marker = use_stamp_marker + rd.use_stamp_marker = use_stamp_marker + rd.use_stamp_note = use_stamp_note + rd.stamp_note_text = stamp_note_text + rd.use_stamp = use_stamp + rd.stamp_font_size = stamp_font_size + rd.stamp_foreground = stamp_foreground + rd.stamp_background = stamp_background + rd.use_stamp_labels = use_stamp_labels + + # Space data settings. + sps.type = shading_type + sps.light = shading_light + sps.studio_light = studio_light + sps.color_type = color_type + sps.background_type = background_type + + sps.show_backface_culling = show_backface_culling + sps.show_xray = show_xray + sps.show_shadows = show_shadows + sps.show_cavity = show_cavity + sps.show_object_outline = show_object_outline + sps.show_specular_highlight = show_specular_highlight + + sp.show_gizmo = show_gizmo \ No newline at end of file diff --git a/blender_kitsu/playblast/ops.py b/blender_kitsu/playblast/ops.py index 119737a9..387344b9 100644 --- a/blender_kitsu/playblast/ops.py +++ b/blender_kitsu/playblast/ops.py @@ -18,7 +18,6 @@ # # (c) 2023, Blender Foundation -import contextlib import webbrowser from pathlib import Path from typing import Dict, List, Set, Optional, Tuple, Any @@ -39,7 +38,7 @@ from blender_kitsu.types import ( TaskStatus, TaskType, ) - +from blender_kitsu.playblast.core import override_render_settings from blender_kitsu.playblast import opsdata logger = LoggerFactory.getLogger() @@ -92,7 +91,7 @@ class KITSU_OT_playblast_create(bpy.types.Operator): context.window_manager.progress_update(0) # Render and save playblast - with self.override_render_settings(context): + with override_render_settings(self, context, context.scene.kitsu.playblast_file): # Get output path. output_path = Path(context.scene.kitsu.playblast_file) @@ -264,169 +263,6 @@ class KITSU_OT_playblast_create(bpy.types.Operator): url = f"{host_url}/productions/{cache.project_active_get().id}/shots?search={cache.shot_active_get().name}" webbrowser.open(url) - @contextlib.contextmanager - def override_render_settings(self, context): - """Overrides the render settings for playblast creation""" - addon_prefs = prefs.addon_prefs_get(context) - rd = context.scene.render - sps = context.space_data.shading - sp = context.space_data - # Get first last name for stamp note text. - session = prefs.session_get(context) - first_name = session.data.user["first_name"] - last_name = session.data.user["last_name"] - # Remember current render settings in order to restore them later. - - # Filepath. - filepath = rd.filepath - - # Format render settings. - percentage = rd.resolution_percentage - file_format = rd.image_settings.file_format - ffmpeg_constant_rate = rd.ffmpeg.constant_rate_factor - ffmpeg_codec = rd.ffmpeg.codec - ffmpeg_format = rd.ffmpeg.format - ffmpeg_audio_codec = rd.ffmpeg.audio_codec - - # Stamp metadata settings. - metadata_input = rd.metadata_input - use_stamp_date = rd.use_stamp_date - use_stamp_time = rd.use_stamp_time - use_stamp_render_time = rd.use_stamp_render_time - use_stamp_frame = rd.use_stamp_frame - use_stamp_frame_range = rd.use_stamp_frame_range - use_stamp_memory = rd.use_stamp_memory - use_stamp_hostname = rd.use_stamp_hostname - use_stamp_camera = rd.use_stamp_camera - use_stamp_lens = rd.use_stamp_lens - use_stamp_scene = rd.use_stamp_scene - use_stamp_marker = rd.use_stamp_marker - use_stamp_marker = rd.use_stamp_marker - use_stamp_note = rd.use_stamp_note - stamp_note_text = rd.stamp_note_text - use_stamp = rd.use_stamp - stamp_font_size = rd.stamp_font_size - stamp_foreground = rd.stamp_foreground - stamp_background = rd.stamp_background - use_stamp_labels = rd.use_stamp_labels - - # Space data settings. - shading_type = sps.type - shading_light = sps.light - studio_light = sps.studio_light - color_type = sps.color_type - background_type = sps.background_type - - show_backface_culling = sps.show_backface_culling - show_xray = sps.show_xray - show_shadows = sps.show_shadows - show_cavity = sps.show_cavity - show_object_outline = sps.show_object_outline - show_specular_highlight = sps.show_specular_highlight - - show_gizmo = sp.show_gizmo - - try: - # Filepath. - rd.filepath = context.scene.kitsu.playblast_file - - # Format render settings. - rd.resolution_percentage = 100 - rd.image_settings.file_format = "FFMPEG" - rd.ffmpeg.constant_rate_factor = "HIGH" - rd.ffmpeg.codec = "H264" - rd.ffmpeg.format = "MPEG4" - rd.ffmpeg.audio_codec = "AAC" - - # Stamp metadata settings. - rd.metadata_input = "SCENE" - rd.use_stamp_date = False - rd.use_stamp_time = False - rd.use_stamp_render_time = False - rd.use_stamp_frame = True - rd.use_stamp_frame_range = False - rd.use_stamp_memory = False - rd.use_stamp_hostname = False - rd.use_stamp_camera = False - rd.use_stamp_lens = True - rd.use_stamp_scene = False - rd.use_stamp_marker = False - rd.use_stamp_marker = False - rd.use_stamp_note = True - rd.stamp_note_text = f"Animator: {first_name} {last_name}" - rd.use_stamp = True - rd.stamp_font_size = 12 - rd.stamp_foreground = (0.8, 0.8, 0.8, 1) - rd.stamp_background = (0, 0, 0, 0.25) - rd.use_stamp_labels = True - - # Space data settings. - sps.type = "SOLID" - sps.light = "STUDIO" - sps.studio_light = "Default" - sps.color_type = "MATERIAL" - sps.background_type = "THEME" - - sps.show_backface_culling = False - sps.show_xray = False - sps.show_shadows = False - sps.show_cavity = False - sps.show_object_outline = False - sps.show_specular_highlight = True - - sp.show_gizmo = False - - yield - - finally: - # Filepath. - rd.filepath = filepath - - # Return the render settings to normal. - rd.resolution_percentage = percentage - rd.image_settings.file_format = file_format - rd.ffmpeg.codec = ffmpeg_codec - rd.ffmpeg.constant_rate_factor = ffmpeg_constant_rate - rd.ffmpeg.format = ffmpeg_format - rd.ffmpeg.audio_codec = ffmpeg_audio_codec - - # Stamp metadata settings. - rd.metadata_input = metadata_input - rd.use_stamp_date = use_stamp_date - rd.use_stamp_time = use_stamp_time - rd.use_stamp_render_time = use_stamp_render_time - rd.use_stamp_frame = use_stamp_frame - rd.use_stamp_frame_range = use_stamp_frame_range - rd.use_stamp_memory = use_stamp_memory - rd.use_stamp_hostname = use_stamp_hostname - rd.use_stamp_camera = use_stamp_camera - rd.use_stamp_lens = use_stamp_lens - rd.use_stamp_scene = use_stamp_scene - rd.use_stamp_marker = use_stamp_marker - rd.use_stamp_marker = use_stamp_marker - rd.use_stamp_note = use_stamp_note - rd.stamp_note_text = stamp_note_text - rd.use_stamp = use_stamp - rd.stamp_font_size = stamp_font_size - rd.stamp_foreground = stamp_foreground - rd.stamp_background = stamp_background - rd.use_stamp_labels = use_stamp_labels - - # Space data settings. - sps.type = shading_type - sps.light = shading_light - sps.studio_light = studio_light - sps.color_type = color_type - sps.background_type = background_type - - sps.show_backface_culling = show_backface_culling - sps.show_xray = show_xray - sps.show_shadows = show_shadows - sps.show_cavity = show_cavity - sps.show_object_outline = show_object_outline - sps.show_specular_highlight = show_specular_highlight - - sp.show_gizmo = show_gizmo class KITSU_OT_playblast_set_version(bpy.types.Operator): diff --git a/blender_kitsu/sqe/ops.py b/blender_kitsu/sqe/ops.py index 559b8b1c..4bd29efc 100644 --- a/blender_kitsu/sqe/ops.py +++ b/blender_kitsu/sqe/ops.py @@ -24,7 +24,7 @@ import colorsys import random from pathlib import Path from typing import Dict, List, Set, Optional, Tuple, Any - +import datetime import bpy from blender_kitsu import gazu, cache, util, prefs, bkglobals @@ -40,6 +40,8 @@ from blender_kitsu.types import ( Task, ) +from blender_kitsu.playblast.core import override_render_path, override_render_format + logger = LoggerFactory.getLogger() @@ -307,7 +309,6 @@ class KITSU_OT_sqe_push_new_shot(bpy.types.Operator): % (noun.lower()), ) - class KITSU_OT_sqe_push_new_sequence(bpy.types.Operator): bl_idname = "kitsu.sqe_push_new_sequence" bl_label = "Submit New Sequence" @@ -1404,11 +1405,7 @@ class KITSU_OT_sqe_push_render(bpy.types.Operator): logger.info("-END- Pushing Sequence Editor Render") return {"FINISHED"} - def _gen_output_path(self, strip: bpy.types.Sequence, task_type: TaskType) -> Path: - addon_prefs = prefs.addon_prefs_get(bpy.context) - folder_name = addon_prefs.sqe_render_dir - file_name = f"{strip.kitsu.shot_id}_{strip.kitsu.shot_name}.{(task_type.name).lower()}.mp4" - return Path(folder_name).absolute().joinpath(file_name) + @contextlib.contextmanager def override_render_settings(self, context, thumbnail_width=256): @@ -2398,7 +2395,132 @@ class KITSU_OT_sqe_change_strip_source(bpy.types.Operator): util.ui_redraw() return {"FINISHED"} + +def set_entity_data(entity, key: str, value: int): + if get_entity_data(entity, key) is not None: + entity['data'][key] = value + return entity +def get_entity_data(entity, key: str): + if entity.get("data").get(key) is not None: + return entity.get("data").get(key) + +def get_dict_len(items:dict): + try: + return len(items) + except TypeError: + return None + +def set_revision_int(prev_rev=None): + if prev_rev is None: + return 1 + return prev_rev+1 +class KITSU_OT_vse_publish_edit_revision(bpy.types.Operator): + bl_idname = "kitsu.vse_publish_edit_revision" + bl_label = "Render and 'Publish as Revision'" + bl_description = "Renders current VSE Edit as .mp4 and publishes as revision on 'Edit Task'" + + def get_edit_entry_items(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]: + sorted_edits = [] + active_project = cache.project_active_get() + + for edit in gazu.edit.get_all_edits_with_tasks(): + if (edit["project_id"] == active_project.id) and not edit['canceled']: + sorted_edits.append(edit) + + return [(item.get("id"), item.get("name"), f'Created at: "{item.get("created_at")}" {item.get("description")}') for item in sorted_edits] + + def get_edit_task_items(self: Any, context: bpy.types.Context) -> List[Tuple[str, str, str]]: + tasks = gazu.task.all_tasks_for_edit(self.edit_entry) + return [(item.get("id"), item.get("name"), f'Created at: "{item.get("created_at")}" {item.get("description")}') for item in tasks] + + comment: bpy.props.StringProperty(name="Comment") + edit_entry: bpy.props.EnumProperty(name="Edit", items=get_edit_entry_items) + task: bpy.props.EnumProperty(name="Edit", items=get_edit_task_items) + render_dir: bpy.props.StringProperty( + name="Folder", + subtype="DIR_PATH", + ) + use_frame_start: bpy.props.BoolProperty(name="Submit update to 'frame_start'.", default=False) + frame_start: bpy.props.IntProperty(name="Frame Start", description="Send an integerfor the 'frame_start' value of the current Kitsu Edit. \nThis is used by Watchtower to pad the edit in the timeline.", default=0) + + @classmethod + def poll(cls, context: bpy.types.Context) -> bool: + return bool( + prefs.session_auth(context) + and cache.project_active_get() + ) + + def invoke(self, context, event): + # Remove file name if set in render.filepath + dir_path = bpy.path.abspath(context.scene.render.filepath) + if not os.path.isdir(Path(dir_path)): + dir_path = Path(dir_path).parent + self.render_dir = str(dir_path) + + #'frame_start' is optionally property appearring on all edit_entries for a project if it exists + server_frame_start = get_entity_data(gazu.edit.get_edit(self.edit_entry), 'frame_start') + self.frame_start = server_frame_start + self.use_frame_start = bool(server_frame_start is not None) + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context: bpy.types.Context) -> None: + layout = self.layout + layout.prop(self, "edit_entry") + if len(self.get_edit_task_items(context)) >= 2: + layout.prop(self, "task") + layout.prop(self, "comment") + layout.prop(self, "render_dir") + + # Only set `frame_start` if exists on current project + if self.use_frame_start: + layout.prop(self, "frame_start") + + + def execute(self, context: bpy.types.Context) -> Set[str]: + if self.task == "": + self.report({"ERROR"}, "Selected edit doesn't have any task associated with it .") + return {"CANCELLED"} + + active_project = cache.project_active_get() + + existing_previews = gazu.edit.get_all_previews_for_edit(self.edit_entry) + len_previews = get_dict_len(existing_previews) + revision = set_revision_int(len_previews) + + # Build render_path + render_dir = bpy.path.abspath(self.render_dir) + if not os.path.isdir(Path(render_dir)): + self.report( + {"ERROR"}, + f"Render path is not set to a directory. '{self.render_dir}'" + ) + return {"CANCELLED"} + edit_entry = gazu.edit.get_edit(self.edit_entry) + render_name = f"{active_project.name}_{edit_entry.get('name')}_v{revision}.mp4" + render_path = Path(render_dir).joinpath(render_name) + + # Render Sequence to .mp4 + with override_render_path(self, context, render_path.as_posix()): + with override_render_format(self, context): + bpy.ops.render.opengl(animation=True, sequencer=True) + + # Create comment with video + task_entity = gazu.task.get_task(self.task) + new_comment = gazu.task.add_comment(task_entity, task_entity["task_status"], self.comment) + new_preview = gazu.task.add_preview(task_entity, new_comment, render_path) + + # Update edit_entry's frame_start if 'frame_start' is found on server + if self.use_frame_start: + edit_entity_update = set_entity_data(edit_entry, 'frame_start', self.frame_start) + updated_edit_entity = gazu.entity.update_entity(edit_entity_update) #TODO add a generic function to update entites + + + self.report( + {"INFO"}, + f"Submitted new comment 'Revision {revision}'" + ) + return {"FINISHED"} # ---------REGISTER ----------. @@ -2429,6 +2551,8 @@ classes = [ KITSU_OT_sqe_scan_for_media_updates, KITSU_OT_sqe_change_strip_source, KITSU_OT_sqe_clear_update_indicators, + KITSU_OT_vse_publish_edit_revision, + ] diff --git a/blender_kitsu/sqe/ui.py b/blender_kitsu/sqe/ui.py index 681a6eee..25584414 100644 --- a/blender_kitsu/sqe/ui.py +++ b/blender_kitsu/sqe/ui.py @@ -735,6 +735,14 @@ class KITSU_PT_sqe_general_tools(bpy.types.Panel): KITSU_OT_sqe_change_strip_source.bl_idname, text="", icon="FILE_PARENT" ).go_latest = True + # Create box. + layout = self.layout + box = layout.box() + box.label(text="Edit Tasks", icon="SEQ_SEQUENCER") + + # Scan for outdated media and reset operator. + row = box.row(align=True) + row.operator("kitsu.vse_publish_edit_revision") # ---------REGISTER ----------.