From 5e3f0ab337dec07f6a76f10267241f1ba1daa40a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:11:05 +0100 Subject: [PATCH 001/327] initial commit --- pype/scripts/slate/base.py | 799 +++++++++++++++++++++++++++++++++++++ 1 file changed, 799 insertions(+) create mode 100644 pype/scripts/slate/base.py diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py new file mode 100644 index 0000000000..cd51c94aab --- /dev/null +++ b/pype/scripts/slate/base.py @@ -0,0 +1,799 @@ +import sys +sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") +from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor +from uuid import uuid4 + +class BaseObj: + """Base Object for slates.""" + + obj_type = None + available_parents = [] + + def __init__(self, parent, style={}, name=None): + if not self.obj_type: + raise NotImplemented( + "Class don't have set object type <{}>".format( + self.__class__.__name__ + ) + ) + + parent_obj_type = None + if parent: + parent_obj_type = parent.obj_type + + if parent_obj_type not in self.available_parents: + expected_parents = ", ".join(self.available_parents) + raise Exception(( + "Invalid parent <{}> for <{}>. Expected <{}>" + ).format( + parent.__class__.__name__, self.obj_type, expected_parents + )) + + self.parent = parent + self._style = style + + self.id = uuid4() + self.parent = parent + self.name = name + self._style = style + self.items = {} + self.final_style = None + + @property + def main_style(self): + default_style_v1 = { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": False, + "font-italic": False, + "bg-color": None, + "bg-alter-color": None, + "alignment-vertical": "left", #left, center, right + "alignment-horizontal": "top", #top, center, bottom + "padding": 0, + # "padding-left": 0, + # "padding-right": 0, + # "padding-top": 0, + # "padding-bottom": 0 + "margin": 0, + # "margin-left": 0, + # "margin-right": 0, + # "margin-top": 0, + # "margin-bottom": 0 + }, + "layer": {}, + "image": {}, + "text": {}, + "table": {}, + "table-item": {}, + "table-item-col-0": {}, + "#MyName": {} + } + return default_style_v1 + + @property + def is_drawing(self): + return self.parent.is_drawing + + @property + def height(self): + raise NotImplementedError( + "Attribute `height` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + @property + def width(self): + raise NotImplementedError( + "Attribute `width` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + @property + def full_style(self): + if self.is_drawing and self.final_style is not None: + return self.final_style + + if self.parent is not None: + style = dict(val for val in self.parent.full_style.items()) + else: + style = self.main_style + + for key, value in self._style.items(): + if key in style: + style[key] = value + style.update() + + if self.is_drawing: + self.final_style = style + + return style + + @property + def style(self): + style = self.full_style + style.update(self._style) + + base = style.get("*", style.get("global")) or {} + obj_specific = style.get(self.obj_type) or {} + name_specific = {} + if self.name: + name = str(self.name) + if not name.startswith("#"): + name += "#" + name_specific = style.get(name) or {} + + output = {} + output.update(base) + output.update(obj_specific) + output.update(name_specific) + + return output + + @property + def pos_x(self): + return 0 + + @property + def pos_y(self): + return 0 + + @property + def pos_start(self): + return (self.pos_x, self.pos_y) + + @property + def pos_end(self): + pos_x, pos_y = self.pos_start + pos_x += self.width + pos_y += self.height + return (pos_x, pos_y) + + @property + def max_width(self): + return self.style.get("max-width") or (self.width * 1) + + @property + def max_height(self): + return self.style.get("max-height") or (self.height * 1) + + @property + def max_content_width(self): + width = self.max_width + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + return (width - (padding_left + padding_right)) + + @property + def max_content_height(self): + height = self.max_height + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + return (height - (padding_top + padding_bottom)) + + def add_item(self, item): + self.items[item.id] = item + + def reset(self): + self.final_style = None + for item in self.items.values(): + item.reset() + + +class MainFrame(BaseObj): + + obj_type = "main_frame" + available_parents = [None] + + def __init__(self, width, height, style={}, parent=None, name=None): + super(MainFrame, self).__init__(parent, style, name) + self._width = width + self._height = height + self._is_drawing = False + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def is_drawing(self): + return self._is_drawing + + def draw(self, path): + self._is_drawing = True + bg_color = self.style["bg-color"] + image = Image.new("RGB", (self.width, self.height))#, color=bg_color) + for item in self.items.values(): + item.draw(image) + + image.save(path) + self._is_drawing = False + self.reset() + + + +class Layer(BaseObj): + obj_type = "layer" + available_parents = ["main_frame", "layer"] + + # Direction can be 0=horizontal/ 1=vertical + def __init__(self, *args, **kwargs): + super(Layer, self).__init__(*args, **kwargs) + + @property + def pos_x(self): + if self.parent.obj_type == self.obj_type: + pos_x = self.parent.item_pos_x(self.id) + else: + pos_x = self.parent.pos_x + return pos_x + + @property + def pos_y(self): + if self.parent.obj_type == self.obj_type: + pos_y = self.parent.item_pos_y(self.id) + else: + pos_y = self.parent.pos_y + return pos_y + + @property + def direction(self): + direction = self.style.get("direction", 0) + if direction not in (0, 1): + raise Exception( + "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" + ) + return direction + + def item_pos_x(self, item_id): + pos_x = self.pos_x + if self.direction != 0: + for id, item in self.items.items(): + if id == item_id: + break + pos_x += item.width + if item.obj_type != "image": + pos_x += 1 + + if pos_x != self.pos_x: + pos_x += 1 + return pos_x + + def item_pos_y(self, item_id): + pos_y = self.pos_y + if self.direction != 1: + for id, item in self.items.items(): + if item_id == id: + break + pos_y += item.height + if item.obj_type != "image": + pos_y += 1 + + return pos_y + + @property + def height(self): + height = 0 + for item in self.items.values(): + if self.direction == 0: + if height > item.height: + continue + + # times 1 because won't get object pointer but number + height = item.height * 1 + else: + height += item.height + + min_height = self.style.get("min-height") + if min_height > height: + return min_height + return height + + @property + def width(self): + width = 0 + for item in self.items.values(): + if self.direction == 0: + if width > item.width: + continue + + # times 1 because won't get object pointer but number + width = item.width * 1 + else: + width += item.width + + min_width = self.style.get("min-width") + if min_width > width: + return min_width + return width + + def draw(self, image, drawer=None): + if drawer is None: + drawer = ImageDraw.Draw(image) + + for item in self.items.values(): + item.draw(image, drawer) + + +class BaseItem(BaseObj): + available_parents = ["layer"] + + def __init__(self, *args, **kwargs): + super(BaseItem, self).__init__(*args, **kwargs) + + @property + def pos_x(self): + return self.parent.item_pos_x(self.id) + + @property + def pos_y(self): + return self.parent.item_pos_y(self.id) + + @property + def content_pos_x(self): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + return self.pos_x + margin_left + padding_left + + @property + def content_pos_y(self): + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + + return self.pos_y + margin_top + padding_top + + @property + def content_width(self): + raise NotImplementedError( + "Attribute is not implemented" + ) + + @property + def content_height(self): + raise NotImplementedError( + "Attribute is not implemented" + ) + + @property + def width(self): + width = self.content_width + + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return ( + width + + margin_left + margin_right + + padding_left + padding_right + ) + + @property + def height(self): + height = self.content_height + + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return ( + height + + margin_bottom + margin_top + + padding_top + padding_bottom + ) + + def add_item(self, *args, **kwargs): + raise Exception("Can't add item to an item, use layers instead.") + + def draw(self, image, drawer): + raise NotImplementedError( + "Method `draw` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + +class ItemImage(BaseItem): + obj_type = "image" + + def __init__(self, image_path, *args, **kwargs): + super(ItemImage, self).__init__(*args, **kwargs) + self.image_path = image_path + + def draw(self, image, drawer): + paste_image = Image.open(self.image_path) + paste_image = paste_image.resize( + (self.content_width, self.content_height), + Image.ANTIALIAS + ) + image.paste( + paste_image, + (self.content_pos_x, self.content_pos_y) + ) + + @property + def content_width(self): + return self.style.get("width") + + @property + def content_height(self): + return self.style.get("height") + + +class ItemText(BaseItem): + obj_type = "text" + + def __init__(self, value, *args, **kwargs): + super(ItemText, self).__init__(*args, **kwargs) + self.value = value + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (self.pos_start, self.pos_end), + fill=bg_color, + outline=None + ) + + text_pos_start = (self.content_pos_x, self.content_pos_y) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + drawer.text( + text_pos_start, + self.value, + font=font, + fill=font_color + ) + + @property + def content_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + width = font.getsize(self.value)[0] + return width + + @property + def content_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + height = font.getsize(self.value)[1] + return height + + +class ItemTable(BaseItem): + + obj_type = "table" + + def __init__(self, values, *args, **kwargs): + super(ItemTable, self).__init__(*args, **kwargs) + self.size_values = None + self.values_by_cords = None + + self.prepare_values(values) + self.calculate_sizes() + + def prepare_values(self, _values): + values = [] + values_by_cords = [] + for row_idx, row in enumerate(_values): + if len(values_by_cords) < row_idx + 1: + for i in range(len(values_by_cords), row_idx + 1): + values_by_cords.append([]) + + for col_idx, col in enumerate(_values[row_idx]): + if len(values_by_cords[row_idx]) < col_idx + 1: + for i in range(len(values_by_cords[row_idx]), col_idx + 1): + values_by_cords[row_idx].append("") + + if not col: + col = "" + col_item = TableField(row_idx, col_idx, col, self) + values_by_cords[row_idx][col_idx] = col_item + values.append(col_item) + + self.values = values + self.values_by_cords = values_by_cords + + def calculate_sizes(self): + row_heights = [] + col_widths = [] + for row_idx, row in enumerate(self.values_by_cords): + row_heights.append(0) + for col_idx, col_item in enumerate(row): + if len(col_widths) < col_idx + 1: + col_widths.append(0) + + _width = col_widths[col_idx] + item_width = col_item.width + if _width < item_width: + col_widths[col_idx] = item_width + + _height = row_heights[row_idx] + item_height = col_item.height + if _height < item_height: + row_heights[row_idx] = item_height + + self.size_values = (row_heights, col_widths) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (self.pos_start, self.pos_end), + fill=bg_color, + outline=None + ) + + for value in self.values: + value.draw(image, drawer) + + @property + def content_width(self): + row_heights, col_widths = self.size_values + width = 0 + for _width in col_widths: + width += _width + + if width != 0: + width -= 1 + return width + + @property + def content_height(self): + row_heights, col_widths = self.size_values + height = 0 + for _height in row_heights: + height += _height + + if height != 0: + height -= 1 + return height + + def pos_info_by_cord(self, cord_x, cord_y): + row_heights, col_widths = self.size_values + pos_x = self.pos_x + pos_y = self.pos_y + width = 0 + height = 0 + for idx, value in enumerate(col_widths): + if cord_y == idx: + width = value + break + pos_x += value + + for idx, value in enumerate(row_heights): + if cord_x == idx: + height = value + break + pos_y += value + + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_top = self.style.get("padding-top") or padding + pos_x += padding_left + pos_y += padding_top + return (pos_x, pos_y, width, height) + + def filled_style(self, cord_x, cord_y): + # TODO CHANGE STYLE BY CORDS + return self.style + + +class TableField(BaseItem): + + obj_type = "table-item" + available_parents = ["table"] + + def __init__(self, cord_x, cord_y, value, *args, **kwargs): + super(TableField, self).__init__(*args, **kwargs) + self.cord_x = cord_x + self.cord_y = cord_y + self.value = value + + @property + def content_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + width = font.getsize(self.value)[0] + 1 + return width + + @property + def content_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + height = font.getsize(self.value)[1] + 1 + return height + + def content_pos_x(self, pos_x, width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + return pos_x + padding_left + + def content_pos_y(self, pos_y, height): + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + return pos_y + padding_top + + def draw(self, image, drawer): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + pos_start = (pos_x, pos_y) + pos_end = (pos_x + width, pos_y + height) + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (pos_start, pos_end), + fill=bg_color, + outline=None + ) + + text_pos_start = ( + self.content_pos_x(pos_x, width), + self.content_pos_y(pos_y, height) + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + drawer.text( + text_pos_start, + self.value, + font=font, + fill=font_color + ) + +if __name__ == "__main__": + main_style = { + "bg-color": "#777777" + } + text_1_style = { + "padding": 10, + "bg-color": "#00ff77" + } + text_2_style = { + "padding": 8, + "bg-color": "#ff0066" + } + text_3_style = { + "padding": 0, + "bg-color": "#ff5500" + } + main = MainFrame(1920, 1080, style=main_style) + layer = Layer(parent=main) + main.add_item(layer) + + text_1 = ItemText("Testing message 1", layer, text_1_style) + text_2 = ItemText("Testing message 2", layer, text_2_style) + text_3 = ItemText("Testing message 3", layer, text_3_style) + table_1_items = [["0", "Output text 1"], ["1", "Output text 2"], ["2", "Output text 3"]] + table_1_style = { + "padding": 8, + "bg-color": "#0077ff" + } + table_1 = ItemTable(table_1_items, layer, table_1_style) + + image_1_style = { + "width": 240, + "height": 120, + "bg-color": "#7733aa" + } + image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" + image_1 = ItemImage(image_1_path, layer, image_1_style) + + layer.add_item(text_1) + layer.add_item(text_2) + layer.add_item(table_1) + layer.add_item(image_1) + layer.add_item(text_3) + dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output2.png" + main.draw(dst) + + print("*** Drawing done :)") + + +style_schema = { + "alignment": { + "description": "Alignment of item", + "type": "string", + "enum": ["left", "right", "center"], + "example": "left" + }, + "font-family": { + "description": "Font type", + "type": "string", + "example": "Arial" + }, + "font-size": { + "description": "Font size", + "type": "integer", + "example": 26 + }, + "font-color": { + "description": "Font color", + "type": "string", + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#ffffff" + }, + "font-bold": { + "description": "Font boldness", + "type": "boolean", + "example": True + }, + "bg-color": { + "description": "Background color, None means transparent", + "type": ["string", None], + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#333" + }, + "bg-alter-color": None, +} From 89a0dd05fd325053df72163ed4727acfd16b8d77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:11:50 +0100 Subject: [PATCH 002/327] added set.json need rework --- pype/scripts/slate/set.json | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 pype/scripts/slate/set.json diff --git a/pype/scripts/slate/set.json b/pype/scripts/slate/set.json new file mode 100644 index 0000000000..e8bba8ac41 --- /dev/null +++ b/pype/scripts/slate/set.json @@ -0,0 +1,59 @@ +{ + "width": 1920, + "height": 1020, + "bg-color": "#000000", + "__bg-color__": "For setting constant color of background. May cause issues if not set when there are not painted spaces.", + "bg-image": null, + "__bg-image__": "May be path to static image???", + "defaults": { + "font-family": "Arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "bg-color": null, + "bg-alter-color": null, + "alignment": "left", + "__alignment[enum]__": ["left", "right", "center"] + }, + "items": [{ + "rel_pos_x": 0.1, + "rel_pos_y": 0.1, + "rel_width": 0.5, + "type": "table", + "col-format": [ + { + "font-size": 12, + "font-color": "#777777", + "alignment": "right" + }, + { + "alignment": "left" + } + ], + "rows": [ + [ + { + "name": "Version", + }, + { + "value": "mei_101_001_0020_slate_NFX_v001", + "font-bold": true + } + ], [ + { + "value": "Date:" + }, + { + "value": "{date}" + } + ] + ] + }, { + "rel_pos_x": 0.1, + "rel_pos_y": 0.1, + "rel_width": 0.5, + "rel_height": 0.5, + "type": "image", + } + ] +} From 18db21c82837ea80f1f6e93f1290443b8489784a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:12:03 +0100 Subject: [PATCH 003/327] first successfull image inspiration --- pype/scripts/slate/slate.py | 355 ++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 pype/scripts/slate/slate.py diff --git a/pype/scripts/slate/slate.py b/pype/scripts/slate/slate.py new file mode 100644 index 0000000000..4e66f68d21 --- /dev/null +++ b/pype/scripts/slate/slate.py @@ -0,0 +1,355 @@ +import textwrap +from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor + + +class TableDraw: + def __init__( + self, image_width, image_height, + rel_pos_x, rel_pos_y, rel_width, + col_fonts=None, col_font_colors=None, + default_font=None, default_font_color=None, + col_alignments=None, rel_col_widths=None, bg_color=None, + alter_bg_color=None, pad=20, + pad_top=None, pad_bottom=None, pad_left=None, pad_right=None + ): + self.image_width = image_width + + pos_x = image_width * rel_pos_x + pos_y = image_height * rel_pos_y + width = image_width * rel_width + + self.pos_x_start = pos_x + self.pos_y_start = pos_y + self.pos_x_end = pos_x + width + self.width = width + + self.rel_col_widths = list(rel_col_widths) + self._col_widths = None + self._col_alignments = col_alignments + + if bg_color and isinstance(bg_color, str): + bg_color = ImageColor.getrgb(bg_color) + + if alter_bg_color and isinstance(alter_bg_color, str): + alter_bg_color = ImageColor.getrgb(alter_bg_color) + + self._bg_color = bg_color + self._alter_bg_color = alter_bg_color + + self.alter_use = False + + if col_fonts: + _col_fonts = [] + for col_font in col_fonts: + font_name, font_size = col_font + font = ImageFont.truetype(font_name, font_size) + _col_fonts.append(font) + col_fonts = _col_fonts + + self._col_fonts = col_fonts + + self._col_font_colors = col_font_colors + + if not default_font: + default_font = ImageFont.truetype("times", 26) + self.default_font = default_font + + if not default_font_color: + default_font_color = "#ffffff" + self.default_font_color = default_font_color + + self.texts = [] + + if pad is None: + pad = 5 + + _pad_top = pad + if pad_top is not None: + _pad_top = pad_top + + _pad_bottom = pad + if pad_bottom is not None: + _pad_bottom = pad_bottom + + _pad_left = pad + if pad_left is not None: + _pad_left = pad_left + + _pad_right = pad + if pad_right is not None: + _pad_right = pad_right + + self.pad_top = _pad_top + self.pad_bottom = _pad_bottom + self.pad_left = _pad_left + self.pad_right = _pad_right + + @property + def col_widths(self): + if self._col_widths is None: + sum_width = 0 + for w in self.rel_col_widths: + sum_width += w + + one_piece = self.width / sum_width + self._col_widths = [] + for w in self.rel_col_widths: + self._col_widths.append(one_piece * w) + + return self._col_widths + + @property + def col_fonts(self): + if self._col_fonts is None: + self._col_fonts = [] + for _ in range(len(self.col_widths)): + self._col_fonts.append(self.default_font) + + elif len(self._col_fonts) < len(self.col_widths): + if isinstance(self._col_fonts, tuple): + self._col_fonts = list(self._col_fonts) + + while len(self._col_fonts) < len(self.col_widths): + self._col_fonts.append(self.default_font) + + return self._col_fonts + + @property + def col_font_colors(self): + if self._col_font_colors is None: + self._col_font_colors = [] + for _ in range(len(self.col_widths)): + self._col_font_colors.append(self.default_font_color) + + elif len(self._col_font_colors) < len(self.col_widths): + if isinstance(self._col_font_colors, tuple): + self._col_font_colors = list(self._col_font_colors) + + while len(self._col_font_colors) < len(self.col_widths): + self._col_font_colors.append(self.default_font_color) + + return self._col_font_colors + + @property + def col_alignments(self): + if self._col_alignments is None: + self._col_alignments = [] + for _ in range(len(self.col_widths)): + self._col_alignments.append("left") + + elif len(self._col_alignments) < len(self.col_widths): + if isinstance(self._col_alignments, tuple): + self._col_alignments = list(self._col_alignments) + + while len(self._col_alignments) < len(self.col_widths): + self._col_alignments.append("left") + + return self._col_alignments + + @property + def bg_color(self): + if self.alter_use is True: + value = self.alter_bg_color + self.alter_use = False + else: + value = self._bg_color + self.alter_use = True + return value + + @property + def alter_bg_color(self): + if self._alter_bg_color: + return self._alter_bg_color + return self.bg_color + + def add_texts(self, texts): + if isinstance(texts, str): + texts = [texts] + + for text in texts: + if isinstance(text, str): + text = [text] + + if len(text) > len(self.rel_col_widths): + for _ in (len(text) - len(self.rel_col_widths)): + self.rel_col_widths.append(1) + for _t in self.texts: + _t.append("") + + self.texts.append(text) + + def draw(self, drawer): + y_pos = self.pos_y_start + for texts in self.texts: + max_height = None + cols_data = [] + for _idx, col in enumerate(texts): + width = self.col_widths[_idx] + font = self.col_fonts[_idx] + lines, line_height = self.lines_height_by_width( + drawer, col, width - self.pad_left - self.pad_right, font + ) + row_height = line_height * len(lines) + if max_height is None or row_height > max_height: + max_height = row_height + + cols_data.append({ + "lines": lines, + "line_height": line_height + }) + + drawer.rectangle( + ( + (self.pos_x_start, y_pos), + ( + self.pos_x_end, + y_pos + max_height + self.pad_top + self.pad_bottom + ) + ), + fill=self.bg_color + ) + + pos_x_start = self.pos_x_start + self.pad_left + for col, col_data in enumerate(cols_data): + lines = col_data["lines"] + line_height = col_data["line_height"] + alignment = self.col_alignments[col] + x_offset = self.col_widths[col] + font = self.col_fonts[col] + font_color = self.col_font_colors[col] + for idx, line_data in enumerate(lines): + line = line_data["text"] + line_width = line_data["width"] + if alignment == "left": + x_start = pos_x_start + self.pad_left + elif alignment == "right": + x_start = ( + pos_x_start + x_offset - line_width - + self.pad_right - self.pad_left + ) + else: + # TODO else + x_start = pos_x_start + self.pad_left + + drawer.text( + ( + x_start, + y_pos + (idx * line_height) + self.pad_top + ), + line, + font=font, + fill=font_color + ) + pos_x_start += x_offset + + y_pos += max_height + self.pad_top + self.pad_bottom + + def lines_height_by_width(self, drawer, text, width, font): + lines = [] + lines.append([part for part in text.split() if part]) + + line = 0 + while True: + thistext = lines[line] + line = line + 1 + if not thistext: + break + newline = [] + + while True: + _textwidth = drawer.textsize(" ".join(thistext), font)[0] + if ( + _textwidth <= width + ): + break + elif _textwidth > width and len(thistext) == 1: + # TODO raise error? + break + + val = thistext.pop(-1) + + if not val: + break + newline.insert(0, val) + + if len(newline) > 0: + lines.append(newline) + else: + break + + _lines = [] + height = None + for line_items in lines: + line = " ".join(line_items) + (width, _height) = drawer.textsize(line, font) + if height is None or height < _height: + height = _height + + _lines.append({ + "width": width, + "text": line + }) + + return (_lines, height) + + +width = 1920 +height = 1080 +# width = 800 +# height = 600 +bg_color_hex = "#242424" +bg_color = ImageColor.getrgb(bg_color_hex) + +base = Image.new('RGB', (width, height), color=bg_color) + +texts = [ + ("Version name:", "mei_101_001_0020_slate_NFX_v001"), + ("Date:", "2019-08-09"), + ("Shot Types:", "2d comp"), + # ("Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.") +] +text_widths_rel = (2, 8) +col_alignments = ("right", "left") +fonts = (("arial", 20), ("times", 26)) +font_colors = ("#999999", "#ffffff") + +table_color_hex = "#212121" +table_alter_color_hex = "#272727" + +drawer = ImageDraw.Draw(base) +table_d = TableDraw( + width, height, + 0.1, 0.1, 0.5, + col_fonts=fonts, + col_font_colors=font_colors, + rel_col_widths=text_widths_rel, + col_alignments=col_alignments, + bg_color=table_color_hex, + alter_bg_color=table_alter_color_hex, + pad_top=20, pad_bottom=20, pad_left=5, pad_right=5 +) + +table_d.add_texts(texts) +table_d.draw(drawer) + +image_path = r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\image.jpg" +image = Image.open(image_path) +img_width, img_height = image.size + +rel_image_width = 0.3 +rel_image_pos_x = 0.65 +rel_image_pos_y = 0.1 +image_pos_x = int(width * rel_image_pos_x) +image_pos_y = int(width * rel_image_pos_y) + +new_width = int(width * rel_image_width) +new_height = int(new_width * img_height / img_width) +image = image.resize((new_width, new_height), Image.ANTIALIAS) + +# mask = Image.new("L", image.size, 255) + +base.paste(image, (image_pos_x, image_pos_y)) +base.save(r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\test{}x{}.jpg".format( + width, height +)) +base.show() From 449fb26aa796681ed77ffb62260ff0dcebc666b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 12 Jan 2020 23:14:40 +0100 Subject: [PATCH 004/327] added margins and modified style --- pype/scripts/slate/base.py | 117 +++++++++----------------- pype/scripts/slate/default_style.json | 44 ++++++++++ pype/scripts/slate/style_schema.json | 44 ++++++++++ 3 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 pype/scripts/slate/default_style.json create mode 100644 pype/scripts/slate/style_schema.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index cd51c94aab..5ae7109ed4 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -3,6 +3,7 @@ sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor from uuid import uuid4 + class BaseObj: """Base Object for slates.""" @@ -11,7 +12,7 @@ class BaseObj: def __init__(self, parent, style={}, name=None): if not self.obj_type: - raise NotImplemented( + raise NotImplementedError( "Class don't have set object type <{}>".format( self.__class__.__name__ ) @@ -50,18 +51,10 @@ class BaseObj: "font-italic": False, "bg-color": None, "bg-alter-color": None, - "alignment-vertical": "left", #left, center, right - "alignment-horizontal": "top", #top, center, bottom + "alignment-vertical": "left", + "alignment-horizontal": "top", "padding": 0, - # "padding-left": 0, - # "padding-right": 0, - # "padding-top": 0, - # "padding-bottom": 0 "margin": 0, - # "margin-left": 0, - # "margin-right": 0, - # "margin-top": 0, - # "margin-bottom": 0 }, "layer": {}, "image": {}, @@ -105,8 +98,9 @@ class BaseObj: for key, value in self._style.items(): if key in style: - style[key] = value - style.update() + style[key].update(value) + else: + style[self.obj_type][key] = value if self.is_drawing: self.final_style = style @@ -134,6 +128,7 @@ class BaseObj: return output + @property def pos_x(self): return 0 @@ -191,8 +186,8 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, style={}, parent=None, name=None): - super(MainFrame, self).__init__(parent, style, name) + def __init__(self, width, height, *args, **kwargs): + super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self._is_drawing = False @@ -212,7 +207,7 @@ class MainFrame(BaseObj): def draw(self, path): self._is_drawing = True bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width, self.height))#, color=bg_color) + image = Image.new("RGB", (self.width, self.height), color=bg_color) for item in self.items.values(): item.draw(image) @@ -221,14 +216,14 @@ class MainFrame(BaseObj): self.reset() - class Layer(BaseObj): obj_type = "layer" available_parents = ["main_frame", "layer"] # Direction can be 0=horizontal/ 1=vertical - def __init__(self, *args, **kwargs): + def __init__(self, direction=0, *args, **kwargs): super(Layer, self).__init__(*args, **kwargs) + self._direction = direction @property def pos_x(self): @@ -248,15 +243,20 @@ class Layer(BaseObj): @property def direction(self): - direction = self.style.get("direction", 0) - if direction not in (0, 1): - raise Exception( + if self._direction not in (0, 1): + print( "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" ) - return direction + return 0 + return self._direction def item_pos_x(self, item_id): pos_x = self.pos_x + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + pos_x += margin_left if self.direction != 0: for id, item in self.items.items(): if id == item_id: @@ -265,12 +265,15 @@ class Layer(BaseObj): if item.obj_type != "image": pos_x += 1 - if pos_x != self.pos_x: - pos_x += 1 return pos_x def item_pos_y(self, item_id): pos_y = self.pos_y + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + + pos_y += margin_top if self.direction != 1: for id, item in self.items.items(): if item_id == id: @@ -283,7 +286,11 @@ class Layer(BaseObj): @property def height(self): - height = 0 + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + height = (margin_top + margin_bottom) for item in self.items.values(): if self.direction == 0: if height > item.height: @@ -294,6 +301,7 @@ class Layer(BaseObj): else: height += item.height + min_height = self.style.get("min-height") if min_height > height: return min_height @@ -301,7 +309,11 @@ class Layer(BaseObj): @property def width(self): - width = 0 + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + width = (margin_left + margin_right) for item in self.items.values(): if self.direction == 0: if width > item.width: @@ -402,9 +414,9 @@ class BaseItem(BaseObj): margin_bottom = self.style.get("margin-bottom") or margin return ( - height + - margin_bottom + margin_top + - padding_top + padding_bottom + height + + margin_bottom + margin_top + + padding_top + padding_bottom ) def add_item(self, *args, **kwargs): @@ -417,6 +429,7 @@ class BaseItem(BaseObj): ) ) + class ItemImage(BaseItem): obj_type = "image" @@ -751,49 +764,3 @@ if __name__ == "__main__": main.draw(dst) print("*** Drawing done :)") - - -style_schema = { - "alignment": { - "description": "Alignment of item", - "type": "string", - "enum": ["left", "right", "center"], - "example": "left" - }, - "font-family": { - "description": "Font type", - "type": "string", - "example": "Arial" - }, - "font-size": { - "description": "Font size", - "type": "integer", - "example": 26 - }, - "font-color": { - "description": "Font color", - "type": "string", - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#ffffff" - }, - "font-bold": { - "description": "Font boldness", - "type": "boolean", - "example": True - }, - "bg-color": { - "description": "Background color, None means transparent", - "type": ["string", None], - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#333" - }, - "bg-alter-color": None, -} diff --git a/pype/scripts/slate/default_style.json b/pype/scripts/slate/default_style.json new file mode 100644 index 0000000000..c0f1006a4a --- /dev/null +++ b/pype/scripts/slate/default_style.json @@ -0,0 +1,44 @@ +{ + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#777777", + "bg-alter-color": "#666666", + "__alignment-vertical__values": ["left", "center", "right"], + "alignment-vertical": "left", + "__alignment-horizontal__values": ["top", "center", "bottom"], + "alignment-horizontal": "top", + "__padding__variants": [ + "padding-top", "padding-right", "padding-bottom", "padding-left" + ], + "padding": 0, + "__margin__variants": [ + "margin-top", "margin-right", "margin-bottom", "margin-left" + ], + "margin": 0, + }, + "layer": { + + }, + "image": { + + }, + "text": { + + }, + "table": { + + }, + "table-item": { + + }, + "table-item-col-0": { + + }, + "#MyName": { + + } +} diff --git a/pype/scripts/slate/style_schema.json b/pype/scripts/slate/style_schema.json new file mode 100644 index 0000000000..dab6151df5 --- /dev/null +++ b/pype/scripts/slate/style_schema.json @@ -0,0 +1,44 @@ +{ + "alignment": { + "description": "Alignment of item", + "type": "string", + "enum": ["left", "right", "center"], + "example": "left" + }, + "font-family": { + "description": "Font type", + "type": "string", + "example": "Arial" + }, + "font-size": { + "description": "Font size", + "type": "integer", + "example": 26 + }, + "font-color": { + "description": "Font color", + "type": "string", + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#ffffff" + }, + "font-bold": { + "description": "Font boldness", + "type": "boolean", + "example": True + }, + "bg-color": { + "description": "Background color, None means transparent", + "type": ["string", None], + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#333" + }, + "bg-alter-color": None, +} From 43fbdbf8fe95809b46f95d361cfd817364197a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jan 2020 12:45:46 +0100 Subject: [PATCH 005/327] added margins --- pype/scripts/slate/base.py | 395 +++++++++++++++++++------------------ 1 file changed, 205 insertions(+), 190 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 5ae7109ed4..bb5d127a2d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -9,6 +9,15 @@ class BaseObj: obj_type = None available_parents = [] + all_style_keys = [ + "font-family", "font-size", "font-color", "font-bold", "font-italic", + "bg-color", "bg-alter-color", + "alignment-vertical", "alignment-horizontal", + "padding", "padding-left", "padding-right", + "padding-top", "padding-bottom", + "margin", "margin-left", "margin-right", + "margin-top", "margin-bottom", "width", "height" + ] def __init__(self, parent, style={}, name=None): if not self.obj_type: @@ -34,9 +43,7 @@ class BaseObj: self._style = style self.id = uuid4() - self.parent = parent self.name = name - self._style = style self.items = {} self.final_style = None @@ -51,18 +58,41 @@ class BaseObj: "font-italic": False, "bg-color": None, "bg-alter-color": None, - "alignment-vertical": "left", + "alignment-vertical": "right", "alignment-horizontal": "top", "padding": 0, "margin": 0, }, - "layer": {}, - "image": {}, - "text": {}, - "table": {}, - "table-item": {}, - "table-item-col-0": {}, - "#MyName": {} + "main_frame": { + "padding": 0, + "margin": 0 + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "image": { + "padding": 0, + "margin": 0 + }, + "text": { + # "alignment-vertical": "left", + # "alignment-horizontal": "top", + "padding": 0, + "margin": 0 + }, + "table": { + "padding": 0, + "margin": 0 + }, + "table-item": { + "padding": 0, + "margin": 0 + }, + "__not_implemented__": { + "table-item-col-0": {}, + "#MyName": {} + } } return default_style_v1 @@ -97,10 +127,18 @@ class BaseObj: style = self.main_style for key, value in self._style.items(): - if key in style: - style[key].update(value) - else: + if key in self.all_style_keys: + # TODO which variant is right? style[self.obj_type][key] = value + # style["*"][key] = value + else: + if key not in style: + style[key] = {} + + if isinstance(style[key], dict): + style[key].update(value) + else: + style[key] = value if self.is_drawing: self.final_style = style @@ -110,9 +148,8 @@ class BaseObj: @property def style(self): style = self.full_style - style.update(self._style) - base = style.get("*", style.get("global")) or {} + base = style.get("*") or {} obj_specific = style.get(self.obj_type) or {} name_specific = {} if self.name: @@ -128,49 +165,121 @@ class BaseObj: return output - @property - def pos_x(self): + def item_pos_x(self): return 0 @property - def pos_y(self): + def item_pos_y(self): return 0 @property - def pos_start(self): - return (self.pos_x, self.pos_y) + def content_pos_x(self): + pos_x = self.item_pos_x + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + return pos_x + margin_left @property - def pos_end(self): - pos_x, pos_y = self.pos_start + def content_pos_y(self): + pos_y = self.item_pos_y + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + return pos_y + margin_top + + @property + def value_pos_x(self): + pos_x = self.content_pos_x * 1 + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + pos_x += padding_left + + return pos_x + + @property + def value_pos_y(self): + pos_y = self.content_pos_y * 1 + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + pos_y += padding_top + + return pos_y + + @property + def value_pos_start(self): + return (self.value_pos_x, self.value_pos_y) + + @property + def value_pos_end(self): + pos_x, pos_y = self.value_pos_start pos_x += self.width pos_y += self.height return (pos_x, pos_y) @property - def max_width(self): - return self.style.get("max-width") or (self.width * 1) + def content_pos_start(self): + return (self.content_pos_x, self.content_pos_y) @property - def max_height(self): - return self.style.get("max-height") or (self.height * 1) + def content_pos_end(self): + pos_x, pos_y = self.content_pos_start + pos_x += self.content_width + pos_y += self.content_height + return (pos_x, pos_y) @property - def max_content_width(self): - width = self.max_width + def value_width(self): + raise NotImplementedError( + "Attribute is not implemented <{}>".format( + self.__class__.__name__ + ) + ) + + @property + def value_height(self): + raise NotImplementedError( + "Attribute is not implemented for <{}>".format( + self.__class__.__name__ + ) + ) + + @property + def content_width(self): + width = self.value_width padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding padding_right = self.style.get("padding-right") or padding - return (width - (padding_left + padding_right)) + return width + padding_left + padding_right @property - def max_content_height(self): - height = self.max_height + def content_height(self): + height = self.value_height padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding padding_bottom = self.style.get("padding-bottom") or padding - return (height - (padding_top + padding_bottom)) + return height + padding_top + padding_bottom + + @property + def width(self): + width = self.content_width + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return width + margin_left + margin_right + + @property + def height(self): + height = self.content_height + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return height + margin_bottom + margin_top def add_item(self, item): self.items[item.id] = item @@ -187,11 +296,26 @@ class MainFrame(BaseObj): available_parents = [None] def __init__(self, width, height, *args, **kwargs): + kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self._is_drawing = False + @property + def value_width(self): + width = 0 + for item in self.items.values(): + width += item.width + return width + + @property + def value_height(self): + height = 0 + for item in self.items.values(): + height += item.height + return height + @property def width(self): return self._width @@ -226,19 +350,19 @@ class Layer(BaseObj): self._direction = direction @property - def pos_x(self): + def item_pos_x(self): if self.parent.obj_type == self.obj_type: - pos_x = self.parent.item_pos_x(self.id) + pos_x = self.parent.child_pos_x(self.id) else: - pos_x = self.parent.pos_x + pos_x = self.parent.value_pos_x return pos_x @property - def pos_y(self): + def item_pos_y(self): if self.parent.obj_type == self.obj_type: - pos_y = self.parent.item_pos_y(self.id) + pos_y = self.parent.child_pos_y(self.id) else: - pos_y = self.parent.pos_y + pos_y = self.parent.value_pos_y return pos_y @property @@ -250,30 +374,21 @@ class Layer(BaseObj): return 0 return self._direction - def item_pos_x(self, item_id): - pos_x = self.pos_x + def child_pos_x(self, item_id): + pos_x = self.value_pos_x - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - pos_x += margin_left if self.direction != 0: for id, item in self.items.items(): - if id == item_id: + if item_id == id: break - pos_x += item.width + pos_x += item.height if item.obj_type != "image": pos_x += 1 - return pos_x - def item_pos_y(self, item_id): - pos_y = self.pos_y + def child_pos_y(self, item_id): + pos_y = self.value_pos_y - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - - pos_y += margin_top if self.direction != 1: for id, item in self.items.items(): if item_id == id: @@ -281,21 +396,15 @@ class Layer(BaseObj): pos_y += item.height if item.obj_type != "image": pos_y += 1 - return pos_y @property - def height(self): - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - height = (margin_top + margin_bottom) + def value_height(self): + height = 0 for item in self.items.values(): if self.direction == 0: if height > item.height: continue - # times 1 because won't get object pointer but number height = item.height * 1 else: @@ -308,24 +417,19 @@ class Layer(BaseObj): return height @property - def width(self): - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - width = (margin_left + margin_right) + def value_width(self): + width = 0 for item in self.items.values(): if self.direction == 0: if width > item.width: continue - # times 1 because won't get object pointer but number width = item.width * 1 else: width += item.width min_width = self.style.get("min-width") - if min_width > width: + if min_width and min_width > width: return min_width return width @@ -344,80 +448,12 @@ class BaseItem(BaseObj): super(BaseItem, self).__init__(*args, **kwargs) @property - def pos_x(self): - return self.parent.item_pos_x(self.id) + def item_pos_x(self): + return self.parent.child_pos_x(self.id) @property - def pos_y(self): - return self.parent.item_pos_y(self.id) - - @property - def content_pos_x(self): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - return self.pos_x + margin_left + padding_left - - @property - def content_pos_y(self): - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - - return self.pos_y + margin_top + padding_top - - @property - def content_width(self): - raise NotImplementedError( - "Attribute is not implemented" - ) - - @property - def content_height(self): - raise NotImplementedError( - "Attribute is not implemented" - ) - - @property - def width(self): - width = self.content_width - - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - return ( - width + - margin_left + margin_right + - padding_left + padding_right - ) - - @property - def height(self): - height = self.content_height - - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - return ( - height - + margin_bottom + margin_top - + padding_top + padding_bottom - ) + def item_pos_y(self): + return self.parent.child_pos_y(self.id) def add_item(self, *args, **kwargs): raise Exception("Can't add item to an item, use layers instead.") @@ -440,20 +476,20 @@ class ItemImage(BaseItem): def draw(self, image, drawer): paste_image = Image.open(self.image_path) paste_image = paste_image.resize( - (self.content_width, self.content_height), + (self.value_width, self.value_height), Image.ANTIALIAS ) image.paste( paste_image, - (self.content_pos_x, self.content_pos_y) + (self.value_pos_x, self.value_pos_y) ) @property - def content_width(self): + def value_width(self): return self.style.get("width") @property - def content_height(self): + def value_height(self): return self.style.get("height") @@ -467,35 +503,27 @@ class ItemText(BaseItem): def draw(self, image, drawer): bg_color = self.style["bg-color"] if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding # TODO border outline styles drawer.rectangle( - (self.pos_start, self.pos_end), + (self.content_pos_start, self.content_pos_end), fill=bg_color, outline=None ) - text_pos_start = (self.content_pos_x, self.content_pos_y) - font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] font = ImageFont.truetype(font_family, font_size) drawer.text( - text_pos_start, + self.value_pos_start, self.value, font=font, fill=font_color ) @property - def content_width(self): + def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -504,7 +532,7 @@ class ItemText(BaseItem): return width @property - def content_height(self): + def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -571,15 +599,9 @@ class ItemTable(BaseItem): def draw(self, image, drawer): bg_color = self.style["bg-color"] if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding # TODO border outline styles drawer.rectangle( - (self.pos_start, self.pos_end), + (self.content_pos_start, self.content_pos_end), fill=bg_color, outline=None ) @@ -588,7 +610,7 @@ class ItemTable(BaseItem): value.draw(image, drawer) @property - def content_width(self): + def value_width(self): row_heights, col_widths = self.size_values width = 0 for _width in col_widths: @@ -599,7 +621,7 @@ class ItemTable(BaseItem): return width @property - def content_height(self): + def value_height(self): row_heights, col_widths = self.size_values height = 0 for _height in row_heights: @@ -611,8 +633,8 @@ class ItemTable(BaseItem): def pos_info_by_cord(self, cord_x, cord_y): row_heights, col_widths = self.size_values - pos_x = self.pos_x - pos_y = self.pos_y + pos_x = self.value_pos_x + pos_y = self.value_pos_y width = 0 height = 0 for idx, value in enumerate(col_widths): @@ -627,12 +649,6 @@ class ItemTable(BaseItem): break pos_y += value - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_top = self.style.get("padding-top") or padding - pos_x += padding_left - pos_y += padding_top return (pos_x, pos_y, width, height) def filled_style(self, cord_x, cord_y): @@ -652,7 +668,7 @@ class TableField(BaseItem): self.value = value @property - def content_width(self): + def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -661,7 +677,7 @@ class TableField(BaseItem): return width @property - def content_height(self): + def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -669,15 +685,19 @@ class TableField(BaseItem): height = font.getsize(self.value)[1] + 1 return height - def content_pos_x(self, pos_x, width): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - return pos_x + padding_left + @property + def item_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + return pos_x - def content_pos_y(self, pos_y, height): - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - return pos_y + padding_top + @property + def item_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + return pos_y def draw(self, image, drawer): pos_x, pos_y, width, height = ( @@ -700,18 +720,13 @@ class TableField(BaseItem): outline=None ) - text_pos_start = ( - self.content_pos_x(pos_x, width), - self.content_pos_y(pos_y, height) - ) - font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] font = ImageFont.truetype(font_family, font_size) drawer.text( - text_pos_start, + self.value_pos_start, self.value, font=font, fill=font_color From 3ef67998f7b3c0c2e80453e0d3dc18bb116bd66c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jan 2020 16:34:15 +0100 Subject: [PATCH 006/327] added initial alignment --- pype/scripts/slate/base.py | 231 +++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 58 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index bb5d127a2d..84b8faf0da 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -12,7 +12,7 @@ class BaseObj: all_style_keys = [ "font-family", "font-size", "font-color", "font-bold", "font-italic", "bg-color", "bg-alter-color", - "alignment-vertical", "alignment-horizontal", + "alignment-horizontal", "alignment-vertical", "padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", @@ -56,10 +56,10 @@ class BaseObj: "font-color": "#ffffff", "font-bold": False, "font-italic": False, - "bg-color": None, - "bg-alter-color": None, - "alignment-vertical": "right", - "alignment-horizontal": "top", + "bg-color": "#0077ff", + "bg-alter-color": "#0055dd", + "alignment-horizontal": "center", + "alignment-vertical": "bottom", "padding": 0, "margin": 0, }, @@ -76,8 +76,6 @@ class BaseObj: "margin": 0 }, "text": { - # "alignment-vertical": "left", - # "alignment-horizontal": "top", "padding": 0, "margin": 0 }, @@ -86,6 +84,7 @@ class BaseObj: "margin": 0 }, "table-item": { + "alignment-horizontal": "right", "padding": 0, "margin": 0 }, @@ -178,15 +177,16 @@ class BaseObj: pos_x = self.item_pos_x margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - return pos_x + margin_left + + pos_x += margin_left + + return pos_x @property def content_pos_y(self): pos_y = self.item_pos_y margin = self.style["margin"] margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin return pos_y + margin_top @property @@ -281,6 +281,30 @@ class BaseObj: return height + margin_bottom + margin_top + # @property + # def max_width(self): + # return self.style.get("max-width") or self.width + # + # @property + # def max_height(self): + # return self.style.get("max-height") or self.height + # + # @property + # def max_content_width(self): + # width = self.max_width + # padding = self.style["padding"] + # padding_left = self.style.get("padding-left") or padding + # padding_right = self.style.get("padding-right") or padding + # return (width - (padding_left + padding_right)) + # + # @property + # def max_content_height(self): + # height = self.max_height + # padding = self.style["padding"] + # padding_top = self.style.get("padding-top") or padding + # padding_bottom = self.style.get("padding-bottom") or padding + # return (height - (padding_top + padding_bottom)) + def add_item(self, item): self.items[item.id] = item @@ -344,7 +368,7 @@ class Layer(BaseObj): obj_type = "layer" available_parents = ["main_frame", "layer"] - # Direction can be 0=horizontal/ 1=vertical + # Direction can be 0=vertical/ 1=horizontal def __init__(self, direction=0, *args, **kwargs): super(Layer, self).__init__(*args, **kwargs) self._direction = direction @@ -369,25 +393,51 @@ class Layer(BaseObj): def direction(self): if self._direction not in (0, 1): print( - "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" + "Direction must be 0 or 1 (0 is horizontal / 1 is vertical)!" ) return 0 return self._direction def child_pos_x(self, item_id): pos_x = self.value_pos_x + alignment_hor = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break if self.direction != 0: - for id, item in self.items.items(): + for id, _item in self.items.items(): if item_id == id: break - pos_x += item.height - if item.obj_type != "image": + pos_x += _item.height + if _item.obj_type != "image": pos_x += 1 - return pos_x + + else: + if alignment_hor in ["center", "centre"]: + pos_x += (self.content_width - item.content_width) / 2 + + elif alignment_hor == "right": + pos_x += self.content_width - item.content_width + + else: + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + pos_x += margin_left + return int(pos_x) def child_pos_y(self, item_id): pos_y = self.value_pos_y + alignment_ver = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break if self.direction != 1: for id, item in self.items.items(): @@ -396,7 +446,19 @@ class Layer(BaseObj): pos_y += item.height if item.obj_type != "image": pos_y += 1 - return pos_y + + else: + if alignment_ver in ["center", "centre"]: + pos_y += (self.content_height - item.content_height) / 2 + + elif alignment_ver == "bottom": + pos_y += self.content_height - item.content_height + + else: + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + pos_y += margin_top + return int(pos_y) @property def value_height(self): @@ -545,7 +607,7 @@ class ItemTable(BaseItem): obj_type = "table" - def __init__(self, values, *args, **kwargs): + def __init__(self, values, use_alternate_color=False, *args, **kwargs): super(ItemTable, self).__init__(*args, **kwargs) self.size_values = None self.values_by_cords = None @@ -553,22 +615,28 @@ class ItemTable(BaseItem): self.prepare_values(values) self.calculate_sizes() + self.use_alternate_color = use_alternate_color + def prepare_values(self, _values): values = [] values_by_cords = [] - for row_idx, row in enumerate(_values): - if len(values_by_cords) < row_idx + 1: - for i in range(len(values_by_cords), row_idx + 1): - values_by_cords.append([]) + row_count = 0 + col_count = 0 + for row in _values: + row_count += 1 + if len(row) > col_count: + col_count = len(row) - for col_idx, col in enumerate(_values[row_idx]): - if len(values_by_cords[row_idx]) < col_idx + 1: - for i in range(len(values_by_cords[row_idx]), col_idx + 1): - values_by_cords[row_idx].append("") - - if not col: + for row_idx in range(row_count): + values_by_cords.append([]) + for col_idx in range(col_count): + values_by_cords[row_idx].append([]) + if col_idx <= len(_values[row_idx]) - 1: + col = _values[row_idx][col_idx] + else: col = "" - col_item = TableField(row_idx, col_idx, col, self) + + col_item = TableField(row_idx, col_idx, col, parent=self) values_by_cords[row_idx][col_idx] = col_item values.append(col_item) @@ -631,7 +699,7 @@ class ItemTable(BaseItem): height -= 1 return height - def pos_info_by_cord(self, cord_x, cord_y): + def content_pos_info_by_cord(self, cord_x, cord_y): row_heights, col_widths = self.size_values pos_x = self.value_pos_x pos_y = self.value_pos_y @@ -669,6 +737,9 @@ class TableField(BaseItem): @property def value_width(self): + if not self.value: + return 0 + font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -678,6 +749,8 @@ class TableField(BaseItem): @property def value_height(self): + if not self.value: + return 0 font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -688,31 +761,68 @@ class TableField(BaseItem): @property def item_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) return pos_x @property def item_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) return pos_y + @property + def value_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + ) + + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor in ["center", "centre"]: + pos_x += (width - self.value_width) / 2 + + elif alignment_hor == "right": + pos_x += width - self.value_width + + else: + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + pos_x += padding_left + + return int(pos_x) + + @property + def value_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + ) + + alignment_ver = self.style["alignment-vertical"].lower() + if alignment_ver in ["center", "centre"]: + pos_y += (height - self.value_height) / 2 + + elif alignment_ver == "bottom": + pos_y += height - self.value_height + + else: + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + pos_y += padding_top + + return int(pos_y) + def draw(self, image, drawer): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) pos_start = (pos_x, pos_y) pos_end = (pos_x + width, pos_y + height) bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] + if self.parent.use_alternate_color and (self.cord_x % 2) == 1: + bg_color = self.style["bg-alter-color"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding + if bg_color and bg_color.lower() != "transparent": # TODO border outline styles drawer.rectangle( (pos_start, pos_end), @@ -734,48 +844,53 @@ class TableField(BaseItem): if __name__ == "__main__": main_style = { - "bg-color": "#777777" + "bg-color": "#777777", + "margin": 0 } text_1_style = { - "padding": 10, + "padding": 0, "bg-color": "#00ff77" } text_2_style = { - "padding": 8, + "padding": 0, "bg-color": "#ff0066" } text_3_style = { "padding": 0, "bg-color": "#ff5500" } - main = MainFrame(1920, 1080, style=main_style) - layer = Layer(parent=main) - main.add_item(layer) - - text_1 = ItemText("Testing message 1", layer, text_1_style) - text_2 = ItemText("Testing message 2", layer, text_2_style) - text_3 = ItemText("Testing message 3", layer, text_3_style) - table_1_items = [["0", "Output text 1"], ["1", "Output text 2"], ["2", "Output text 3"]] - table_1_style = { - "padding": 8, - "bg-color": "#0077ff" - } - table_1 = ItemTable(table_1_items, layer, table_1_style) - image_1_style = { "width": 240, "height": 120, "bg-color": "#7733aa" } + table_1_style = { + "padding": 0, + "bg-color": "#0077ff" + } + + main = MainFrame(1920, 1080, style=main_style) + layer = Layer(parent=main) + main.add_item(layer) + + text_1 = ItemText("Testing message 1", layer, text_1_style) + text_2 = ItemText("Testing 2", layer, text_2_style) + text_3 = ItemText("Testing message 3", layer, text_3_style) + + table_1_items = [["0", "Output text 1", "ha"], ["1", "Output 2"], ["2", "Output text 3"]] + table_1 = ItemTable(table_1_items, True, parent=layer, style=table_1_style) + image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" image_1 = ItemImage(image_1_path, layer, image_1_style) layer.add_item(text_1) layer.add_item(text_2) + layer.add_item(text_3) + layer.add_item(table_1) layer.add_item(image_1) - layer.add_item(text_3) - dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output2.png" + + dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" main.draw(dst) print("*** Drawing done :)") From 8899ce974fbfe91c69e87b5a390910a71e044a75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:33:59 +0100 Subject: [PATCH 007/327] added important imports --- pype/scripts/slate/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 84b8faf0da..33266bf62d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -1,4 +1,9 @@ +import os import sys +import re +import copy +import json +from queue import Queue sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor from uuid import uuid4 From de2d156fc4955f1d4e246a5a0b74b4e5b263d58b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:34:26 +0100 Subject: [PATCH 008/327] base object may have pos_x and pos_y --- pype/scripts/slate/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 33266bf62d..1aca5062be 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -24,7 +24,7 @@ class BaseObj: "margin-top", "margin-bottom", "width", "height" ] - def __init__(self, parent, style={}, name=None): + def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): if not self.obj_type: raise NotImplementedError( "Class don't have set object type <{}>".format( @@ -50,7 +50,9 @@ class BaseObj: self.id = uuid4() self.name = name self.items = {} - self.final_style = None + + self._pos_x = pos_x or 0 + self._pos_y = pos_y or 0 @property def main_style(self): From 549898730a60c099b52f958fed5b24d242f59284 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:35:51 +0100 Subject: [PATCH 009/327] style property use get_style_for_obj_type method --- pype/scripts/slate/base.py | 77 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1aca5062be..6430e5d8f3 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -151,12 +151,12 @@ class BaseObj: return style - @property - def style(self): - style = self.full_style + def get_style_for_obj_type(self, obj_type, style=None): + if not style: + style = copy.deepcopy(self.full_style) base = style.get("*") or {} - obj_specific = style.get(self.obj_type) or {} + obj_specific = style.get(obj_type) or {} name_specific = {} if self.name: name = str(self.name) @@ -164,6 +164,67 @@ class BaseObj: name += "#" name_specific = style.get(name) or {} + + if obj_type == "table-item": + col_regex = r"table-item-col\[([\d\-, ]+)*\]" + row_regex = r"table-item-row\[([\d\-, ]+)*\]" + field_regex = ( + r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" + ) + # STRICT field regex (not allowed spaces) + # fild_regex = r"table-item-field\[(\d+:\d+)*\]" + + def get_indexes_from_regex_match(result, field=False): + group = result.group(1) + indexes = [] + if field: + return [ + int(part.strip()) for part in group.strip().split(":") + ] + + parts = group.strip().split(",") + for part in parts: + part = part.strip() + if "-" not in part: + indexes.append(int(part)) + continue + + sub_parts = [ + int(sub.strip()) for sub in part.split("-") + ] + if len(sub_parts) != 2: + # TODO logging + print("invalid range '{}'".format(part)) + continue + + for idx in range(sub_parts[0], sub_parts[1]+1): + indexes.append(idx) + return indexes + + for key, value in style.items(): + if not key.startswith(obj_type): + continue + + result = re.search(col_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.col_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(row_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.row_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(field_regex, key) + if result: + col_idx, row_idx = get_indexes_from_regex_match(result) + if self.row_idx == col_idx and self.row_idx == row_idx: + obj_specific.update(value) + output = {} output.update(base) output.update(obj_specific) @@ -171,12 +232,20 @@ class BaseObj: return output + @property + def style(self): + return self.get_style_for_obj_type(self.obj_type) + @property def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return 0 @property def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return 0 @property From 7d40f571d58455d724a03f37fa190ed9ccc80184 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:39:02 +0100 Subject: [PATCH 010/327] removed final_style, is_drawing and cleaned up little bit --- pype/scripts/slate/base.py | 54 +++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 6430e5d8f3..737d92de67 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -102,10 +102,6 @@ class BaseObj: } return default_style_v1 - @property - def is_drawing(self): - return self.parent.is_drawing - @property def height(self): raise NotImplementedError( @@ -124,9 +120,6 @@ class BaseObj: @property def full_style(self): - if self.is_drawing and self.final_style is not None: - return self.final_style - if self.parent is not None: style = dict(val for val in self.parent.full_style.items()) else: @@ -146,9 +139,6 @@ class BaseObj: else: style[key] = value - if self.is_drawing: - self.final_style = style - return style def get_style_for_obj_type(self, obj_type, style=None): @@ -385,7 +375,6 @@ class BaseObj: self.items[item.id] = item def reset(self): - self.final_style = None for item in self.items.values(): item.reset() @@ -395,12 +384,12 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, *args, **kwargs): + def __init__(self, width, height, destination_path=None, *args, **kwargs): kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height - self._is_drawing = False + self.dst_path = destination_path @property def value_width(self): @@ -424,19 +413,27 @@ class MainFrame(BaseObj): def height(self): return self._height - @property - def is_drawing(self): - return self._is_drawing + def draw(self, path=None): + if not path: + path = self.dst_path + + if not path: + raise TypeError(( + "draw() missing 1 required positional argument: 'path'" + " if 'destination_path is not specified'" + )) + + dir_path = os.path.dirname(path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) - def draw(self, path): - self._is_drawing = True bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width, self.height), color=bg_color) + image = Image.new("RGB", (self.width(), self.height()), color=bg_color) + drawer = ImageDraw.Draw(image) for item in self.items.values(): - item.draw(image) + item.draw(image, drawer) image.save(path) - self._is_drawing = False self.reset() @@ -571,26 +568,27 @@ class Layer(BaseObj): return min_width return width - def draw(self, image, drawer=None): - if drawer is None: - drawer = ImageDraw.Draw(image) - + def draw(self, image, drawer): for item in self.items.values(): item.draw(image, drawer) class BaseItem(BaseObj): - available_parents = ["layer"] + available_parents = ["main_frame", "layer"] def __init__(self, *args, **kwargs): super(BaseItem, self).__init__(*args, **kwargs) @property def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return self.parent.child_pos_x(self.id) @property def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_y return self.parent.child_pos_y(self.id) def add_item(self, *args, **kwargs): @@ -795,10 +793,6 @@ class ItemTable(BaseItem): return (pos_x, pos_y, width, height) - def filled_style(self, cord_x, cord_y): - # TODO CHANGE STYLE BY CORDS - return self.style - class TableField(BaseItem): From 2b199538c461381d5939b8291d1fc1f1653988c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:26:47 +0100 Subject: [PATCH 011/327] return self._pos_x if parent in main_frame --- pype/scripts/slate/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 737d92de67..e985d9fd04 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -450,6 +450,8 @@ class Layer(BaseObj): def item_pos_x(self): if self.parent.obj_type == self.obj_type: pos_x = self.parent.child_pos_x(self.id) + elif self.parent.obj_type == "main_frame": + pos_x = self._pos_x else: pos_x = self.parent.value_pos_x return pos_x @@ -458,6 +460,8 @@ class Layer(BaseObj): def item_pos_y(self): if self.parent.obj_type == self.obj_type: pos_y = self.parent.child_pos_y(self.id) + elif self.parent.obj_type == "main_frame": + pos_y = self._pos_y else: pos_y = self.parent.value_pos_y return pos_y From ef609344dfddc2b0dd356362eac8c295b7c2bf5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:30:12 +0100 Subject: [PATCH 012/327] initial netflix version json --- pype/scripts/slate/netflix_v01.1.json | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 pype/scripts/slate/netflix_v01.1.json diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.1.json new file mode 100644 index 0000000000..a2be6d13e0 --- /dev/null +++ b/pype/scripts/slate/netflix_v01.1.json @@ -0,0 +1,141 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v001.png", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top", + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "bg-color": "#252525" + }, + "table": { + "bg-color": "transparent" + }, + "table-item": { + "bg-color": "#272727", + "bg-alter-color": "#212121", + "font-color": "#ffffff", + "font-bold": true, + "font-italic": false, + "padding": 5, + "padding-bottom": 10, + "alignment-vertical": "top", + "alignment-horizontal": "left", + "word-wrap": true, + "ellide": true, + "max-lines": 3 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989" + }, + "table-item-col[1]": { + "font-size": 30, + "padding-left": 20 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "pos_x": 40, + "pos_y": 40, + "style": { + "width": 1094, + "height": 1000 + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ], + "style": { + "table-item-col[0]": { + "width": 300 + } + } + }, { + "type": "rectangle", + "style": { + "bg-color": "#d40914", + "width": 1094, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "mei_101_001_0020_slate_NFX_v001"], + ["Date:", "2019-08-09"], + ["Shot Types:", "2d comp"], + ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 300 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 786 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "pos_x": 1174, + "pos_y": 40, + "style": { + "width": 733, + "height": 1000 + }, + "items": [{ + "type": "image", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "style": { + "width": 733, + "height": 414 + } + }, { + "type": "image", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "style": { + "width": 733, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "DAZZLE"], + ["Shot Name:", "SLATE_SIMPLE"], + ["Frames:", "0 - 1 (2)"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left" + }, + "table-item-col[1]": { + "alignment-horizontal": "right" + } + } + }] + }] +} From b872ee29919dd568e9ae935870e6ea6f708028b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:30:54 +0100 Subject: [PATCH 013/327] added argument when processing field style --- pype/scripts/slate/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index e985d9fd04..467b504ca1 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -211,7 +211,9 @@ class BaseObj: result = re.search(field_regex, key) if result: - col_idx, row_idx = get_indexes_from_regex_match(result) + col_idx, row_idx = get_indexes_from_regex_match( + result, True + ) if self.row_idx == col_idx and self.row_idx == row_idx: obj_specific.update(value) From 8c5fd923fe5e4581e53febb930b7d1ee422b54a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:32:17 +0100 Subject: [PATCH 014/327] created FontFactory --- pype/scripts/slate/base.py | 201 ++++++++++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 27 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 467b504ca1..37d59f13bf 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -918,32 +918,102 @@ class TableField(BaseItem): fill=font_color ) -if __name__ == "__main__": - main_style = { - "bg-color": "#777777", - "margin": 0 - } - text_1_style = { - "padding": 0, - "bg-color": "#00ff77" - } - text_2_style = { - "padding": 0, - "bg-color": "#ff0066" - } - text_3_style = { - "padding": 0, - "bg-color": "#ff5500" - } - image_1_style = { - "width": 240, - "height": 120, - "bg-color": "#7733aa" - } - table_1_style = { - "padding": 0, - "bg-color": "#0077ff" - } + +class FontFactory: + fonts = None + default = None + + @classmethod + def get_font(cls, family, font_size=None, italic=False, bold=False): + if cls.fonts is None: + cls.load_fonts() + + styles = [] + if bold: + styles.append("Bold") + + if italic: + styles.append("Italic") + + if not styles: + styles.append("Regular") + + style = " ".join(styles) + family = family.lower() + family_styles = cls.fonts.get(family) + if not family_styles: + return cls.default + + font = family_styles.get(style) + if font: + if font_size: + font = font.font_variant(size=font_size) + return font + + # Return first found + for font in family_styles: + if font_size: + font = font.font_variant(size=font_size) + return font + + return cls.default + + @classmethod + def load_fonts(cls): + + cls.default = ImageFont.load_default() + + available_font_ext = [".ttf", ".ttc"] + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + + elif sys.platform in ("linux", "linux2"): + lindirs = os.environ.get("XDG_DATA_DIRS", "") + if not lindirs: + # According to the freedesktop spec, XDG_DATA_DIRS should + # default to /usr/share + lindirs = "/usr/share" + dirs += [ + os.path.join(lindir, "fonts") for lindir in lindirs.split(":") + ] + + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts") + ] + + available_fonts = collections.defaultdict(dict) + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + ext = os.path.splitext(walkfilename)[1] + if ext.lower() not in available_font_ext: + continue + + fontpath = os.path.join(walkroot, walkfilename) + font_obj = ImageFont.truetype(fontpath) + family = font_obj.font.family.lower() + style = font_obj.font.style + available_fonts[family][style] = font_obj + + cls.fonts = available_fonts + + +def main_v01(): + main_style = {"bg-color": "#777777", "margin": 0} + text_1_style = {"padding": 0, "bg-color": "#00ff77"} + text_2_style = {"padding": 0, "bg-color": "#ff0066"} + text_3_style = {"padding": 0, "bg-color": "#ff5500"} + image_1_style = {"width": 240, "height": 120, "bg-color": "#7733aa"} + table_1_style = {"padding": 0, "bg-color": "#0077ff"} main = MainFrame(1920, 1080, style=main_style) layer = Layer(parent=main) @@ -969,4 +1039,81 @@ if __name__ == "__main__": dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" main.draw(dst) - print("*** Drawing done :)") + +def main_v02(): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + input_json = os.path.join(cur_folder, "netflix_v01.1.json") + with open(input_json) as json_file: + slate_data = json.load(json_file) + + width = slate_data["width"] + height = slate_data["height"] + style = slate_data.get("style") or {} + dst_path = slate_data.get("destination_path") + main = MainFrame(width, height, destination_path=dst_path, style=style) + + load_queue = Queue() + for item in slate_data["items"]: + load_queue.put((item, main)) + + all_objs = [] + while not load_queue.empty(): + item_data, parent = load_queue.get() + + item_type = item_data["type"].lower() + item_style = item_data.get("style", {}) + item_name = item_data.get("name") + + pos_x = None + pos_y = None + if parent.obj_type == "main_frame": + pos_x = item_data.get("pos_x", {}) + pos_y = item_data.get("pos_y", {}) + + kwargs = { + "parent": parent, + "style": item_style, + "pos_x": pos_x, + "pos_y": pos_y + } + + item_obj = None + if item_type == "layer": + direction = item_data.get("direction", 0) + item_obj = Layer(direction, **kwargs) + for item in item_data.get("items", []): + load_queue.put((item, item_obj)) + + elif item_type == "table": + use_alternate_color = item_data.get("use_alternate_color", False) + values = item_data.get("values") or [] + item_obj = ItemTable(values, use_alternate_color, **kwargs) + + elif item_type == "image": + path = item_data["path"] + item_obj = ItemImage(path, **kwargs) + + elif item_type == "rectangle": + item_obj = ItemRectangle(**kwargs) + + if not item_obj: + print( + "Slate item not implemented <{}> - skipping".format(item_type) + ) + continue + + all_objs.append(item_obj) + + parent.add_item(item_obj) + + main.draw() + # for item in all_objs: + # print(item.style.get("width"), item.style.get("height")) + # print(item.width, item.height) + # print(item.content_pos_x, item.content_pos_y) + # print(item.value_pos_x, item.value_pos_y) + + +if __name__ == "__main__": + main_v02() + print("*** Drawing is done") From 7a800142077bc2c580366344e3dd739070ae2185 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:37:32 +0100 Subject: [PATCH 015/327] all heights and widths are callable and not property --- pype/scripts/slate/base.py | 82 ++++++++++++++------------------------ 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 37d59f13bf..750061a844 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -102,7 +102,6 @@ class BaseObj: } return default_style_v1 - @property def height(self): raise NotImplementedError( "Attribute `height` is not implemented for <{}>".format( @@ -110,7 +109,6 @@ class BaseObj: ) ) - @property def width(self): raise NotImplementedError( "Attribute `width` is not implemented for <{}>".format( @@ -282,8 +280,8 @@ class BaseObj: @property def value_pos_end(self): pos_x, pos_y = self.value_pos_start - pos_x += self.width - pos_y += self.height + pos_x += self.width() + pos_y += self.height() return (pos_x, pos_y) @property @@ -293,11 +291,10 @@ class BaseObj: @property def content_pos_end(self): pos_x, pos_y = self.content_pos_start - pos_x += self.content_width - pos_y += self.content_height + pos_x += self.content_width() + pos_y += self.content_height() return (pos_x, pos_y) - @property def value_width(self): raise NotImplementedError( "Attribute is not implemented <{}>".format( @@ -305,7 +302,6 @@ class BaseObj: ) ) - @property def value_height(self): raise NotImplementedError( "Attribute is not implemented for <{}>".format( @@ -313,25 +309,22 @@ class BaseObj: ) ) - @property def content_width(self): - width = self.value_width + width = self.value_width() padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding padding_right = self.style.get("padding-right") or padding return width + padding_left + padding_right - @property def content_height(self): - height = self.value_height + height = self.value_height() padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding padding_bottom = self.style.get("padding-bottom") or padding return height + padding_top + padding_bottom - @property def width(self): - width = self.content_width + width = self.content_width() margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin @@ -339,9 +332,8 @@ class BaseObj: return width + margin_left + margin_right - @property def height(self): - height = self.content_height + height = self.content_height() margin = self.style["margin"] margin_top = self.style.get("margin-top") or margin @@ -393,25 +385,21 @@ class MainFrame(BaseObj): self._height = height self.dst_path = destination_path - @property def value_width(self): width = 0 for item in self.items.values(): - width += item.width + width += item.width() return width - @property def value_height(self): height = 0 for item in self.items.values(): - height += item.height + height += item.height() return height - @property def width(self): return self._width - @property def height(self): return self._height @@ -491,16 +479,17 @@ class Layer(BaseObj): for id, _item in self.items.items(): if item_id == id: break - pos_x += _item.height + + pos_x += _item.height() if _item.obj_type != "image": pos_x += 1 else: if alignment_hor in ["center", "centre"]: - pos_x += (self.content_width - item.content_width) / 2 + pos_x += (self.content_width() - item.content_width()) / 2 elif alignment_hor == "right": - pos_x += self.content_width - item.content_width + pos_x += self.content_width() - item.content_width() else: margin = self.style["margin"] @@ -522,16 +511,16 @@ class Layer(BaseObj): for id, item in self.items.items(): if item_id == id: break - pos_y += item.height + pos_y += item.height() if item.obj_type != "image": pos_y += 1 else: if alignment_ver in ["center", "centre"]: - pos_y += (self.content_height - item.content_height) / 2 + pos_y += (self.content_height() - item.content_height()) / 2 elif alignment_ver == "bottom": - pos_y += self.content_height - item.content_height + pos_y += self.content_height() - item.content_height() else: margin = self.style["margin"] @@ -539,35 +528,33 @@ class Layer(BaseObj): pos_y += margin_top return int(pos_y) - @property def value_height(self): height = 0 for item in self.items.values(): if self.direction == 0: - if height > item.height: + if height > item.height(): continue # times 1 because won't get object pointer but number - height = item.height * 1 + height = item.height() else: - height += item.height + height += item.height() min_height = self.style.get("min-height") - if min_height > height: + if min_height and min_height > height: return min_height return height - @property def value_width(self): width = 0 for item in self.items.values(): if self.direction == 0: - if width > item.width: + if width > item.width(): continue # times 1 because won't get object pointer but number - width = item.width * 1 + width = item.width() else: - width += item.width + width += item.width() min_width = self.style.get("min-width") if min_width and min_width > width: @@ -626,11 +613,9 @@ class ItemImage(BaseItem): (self.value_pos_x, self.value_pos_y) ) - @property def value_width(self): return self.style.get("width") - @property def value_height(self): return self.style.get("height") @@ -664,7 +649,6 @@ class ItemText(BaseItem): fill=font_color ) - @property def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -673,7 +657,6 @@ class ItemText(BaseItem): width = font.getsize(self.value)[0] return width - @property def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -733,12 +716,12 @@ class ItemTable(BaseItem): col_widths.append(0) _width = col_widths[col_idx] - item_width = col_item.width + item_width = col_item.width() if _width < item_width: col_widths[col_idx] = item_width _height = row_heights[row_idx] - item_height = col_item.height + item_height = col_item.height() if _height < item_height: row_heights[row_idx] = item_height @@ -757,7 +740,6 @@ class ItemTable(BaseItem): for value in self.values: value.draw(image, drawer) - @property def value_width(self): row_heights, col_widths = self.size_values width = 0 @@ -768,7 +750,6 @@ class ItemTable(BaseItem): width -= 1 return width - @property def value_height(self): row_heights, col_widths = self.size_values height = 0 @@ -811,7 +792,6 @@ class TableField(BaseItem): self.cord_y = cord_y self.value = value - @property def value_width(self): if not self.value: return 0 @@ -823,7 +803,6 @@ class TableField(BaseItem): width = font.getsize(self.value)[0] + 1 return width - @property def value_height(self): if not self.value: return 0 @@ -853,13 +832,12 @@ class TableField(BaseItem): pos_x, pos_y, width, height = ( self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) - alignment_hor = self.style["alignment-horizontal"].lower() if alignment_hor in ["center", "centre"]: - pos_x += (width - self.value_width) / 2 + pos_x += (width - self.value_width()) / 2 elif alignment_hor == "right": - pos_x += width - self.value_width + pos_x += width - self.value_width() else: padding = self.style["padding"] @@ -876,10 +854,10 @@ class TableField(BaseItem): alignment_ver = self.style["alignment-vertical"].lower() if alignment_ver in ["center", "centre"]: - pos_y += (height - self.value_height) / 2 + pos_y += (height - self.value_height()) / 2 elif alignment_ver == "bottom": - pos_y += height - self.value_height + pos_y += height - self.value_height() else: padding = self.style["padding"] From 25cb9b82896e343ca76793f47582fcfb7107fc54 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:38:30 +0100 Subject: [PATCH 016/327] added rectangle item --- pype/scripts/slate/base.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 750061a844..4029379655 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -618,6 +618,35 @@ class ItemImage(BaseItem): def value_height(self): return self.style.get("height") +class ItemRectangle(BaseItem): + obj_type = "rectangle" + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + fill = self.style.get("fill", False) + kwargs = {} + if fill: + kwargs["fill"] = bg_color + else: + kwargs["outline"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) class ItemText(BaseItem): From 184e1f17615fbfd3b35f3565ab3262b023cedcde Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:39:37 +0100 Subject: [PATCH 017/327] cleanup --- pype/scripts/slate/base.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 4029379655..7912b93026 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -21,7 +21,8 @@ class BaseObj: "padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", - "margin-top", "margin-bottom", "width", "height" + "margin-top", "margin-bottom", "width", "height", + "fill" ] def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): @@ -229,13 +230,13 @@ class BaseObj: @property def item_pos_x(self): if self.parent.obj_type == "main_frame": - return self._pos_x + return int(self._pos_x) return 0 @property def item_pos_y(self): if self.parent.obj_type == "main_frame": - return self._pos_x + return int(self._pos_y) return 0 @property @@ -257,7 +258,7 @@ class BaseObj: @property def value_pos_x(self): - pos_x = self.content_pos_x * 1 + pos_x = int(self.content_pos_x) padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding pos_x += padding_left @@ -266,7 +267,7 @@ class BaseObj: @property def value_pos_y(self): - pos_y = self.content_pos_y * 1 + pos_y = int(self.content_pos_y) padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding pos_y += padding_top @@ -444,7 +445,7 @@ class Layer(BaseObj): pos_x = self._pos_x else: pos_x = self.parent.value_pos_x - return pos_x + return int(pos_x) @property def item_pos_y(self): @@ -454,7 +455,7 @@ class Layer(BaseObj): pos_y = self._pos_y else: pos_y = self.parent.value_pos_y - return pos_y + return int(pos_y) @property def direction(self): @@ -569,8 +570,6 @@ class Layer(BaseObj): class BaseItem(BaseObj): available_parents = ["main_frame", "layer"] - def __init__(self, *args, **kwargs): - super(BaseItem, self).__init__(*args, **kwargs) @property def item_pos_x(self): @@ -603,9 +602,9 @@ class ItemImage(BaseItem): self.image_path = image_path def draw(self, image, drawer): - paste_image = Image.open(self.image_path) - paste_image = paste_image.resize( - (self.value_width, self.value_height), + source_image = Image.open(os.path.normpath(self.image_path)) + paste_image = source_image.resize( + (self.value_width(), self.value_height()), Image.ANTIALIAS ) image.paste( @@ -614,10 +613,12 @@ class ItemImage(BaseItem): ) def value_width(self): - return self.style.get("width") + return int(self.style["width"]) def value_height(self): - return self.style.get("height") + return int(self.style["height"]) + + class ItemRectangle(BaseItem): obj_type = "rectangle" @@ -684,7 +685,7 @@ class ItemText(BaseItem): font = ImageFont.truetype(font_family, font_size) width = font.getsize(self.value)[0] - return width + return int(width) def value_height(self): font_family = self.style["font-family"] @@ -692,7 +693,7 @@ class ItemText(BaseItem): font = ImageFont.truetype(font_family, font_size) height = font.getsize(self.value)[1] - return height + return int(height) class ItemTable(BaseItem): From 9dd0079b28669454a1ff446c8459d47ae4a2866d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:41:08 +0100 Subject: [PATCH 018/327] use font factory for fonts --- pype/scripts/slate/base.py | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 7912b93026..1724984fce 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -670,8 +670,12 @@ class ItemText(BaseItem): font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) drawer.text( self.value_pos_start, self.value, @@ -682,16 +686,24 @@ class ItemText(BaseItem): def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) width = font.getsize(self.value)[0] return int(width) def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) height = font.getsize(self.value)[1] return int(height) @@ -828,20 +840,28 @@ class TableField(BaseItem): font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) - width = font.getsize(self.value)[0] + 1 - return width + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize_multiline(self.value)[0] + 1 + return int(width) def value_height(self): if not self.value: return 0 font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) - height = font.getsize(self.value)[1] + 1 - return height + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize_multiline(self.value)[1] + 1 + return int(height) @property def item_pos_x(self): @@ -917,8 +937,12 @@ class TableField(BaseItem): font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) drawer.text( self.value_pos_start, self.value, From 435713a076dd2e4d644a1b071472d6ce976df5eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 13:50:42 +0100 Subject: [PATCH 019/327] added recalculate method for word wrapping --- pype/scripts/slate/base.py | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1724984fce..c83394ce54 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -834,6 +834,108 @@ class TableField(BaseItem): self.cord_y = cord_y self.value = value + def recalculate_by_width(self, value, max_width): + if not value: + return "" + + word_wrap = self.style.get("word-wrap") + ellide = self.style.get("ellide") + max_lines = self.style.get("max-lines") + + if not ellide and not word_wrap: + # TODO logging + print(( + "Can't draw text because is too long with" + " `word-wrap` and `ellide` turned off" + )) + return "" + + elif ellide and not word_wrap: + max_lines = 1 + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + + words = [word for word in value.split()] + words_len = len(words) + lines = [] + last_index = 0 + while True: + line = "" + for idx in range(last_index, words_len): + _word = words[idx] + _line = " ".join([line, _word]) + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + break + line = _line + last_index = idx + + if line: + lines.append(line) + + if last_index == words_len - 1: + break + + elif last_index == 0: + if ellide: + line = "" + for idx, char in enumerate(words[idx]): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + + lines.append(line) + # TODO logging + print("Font size is too big.") + break + + output = "" + if not lines: + return output + + if max_lines and len(lines) > max_lines: + lines = [lines[idx] for idx in range(max_lines)] + if not ellide: + return "\n".join(lines) + + last_line = lines[-1] + last_line_width = font.getsize(last_line + self.ellide_text)[0] + if last_line_width <= max_width: + lines[-1] += self.ellide_text + return "\n".join([line for line in lines]) + + last_line_words = last_line.split() + if len(last_line_words) == 1: + if max_lines > 1: + lines[-1] = self.ellide_text + return "\n".join([line for line in lines]) + + _line = "" + for idx, char in enumerate(last_line): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + lines[-1] = line + return "\n".join([line for line in lines]) + + return "\n".join([line for line in lines]) + + def value_width(self): if not self.value: return 0 From 96d22f8a9be81ceb7bcf094b77e7f67bf568a201 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:01:20 +0100 Subject: [PATCH 020/327] added default ellide text --- pype/scripts/slate/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index c83394ce54..b6e4bd2701 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -827,6 +827,7 @@ class TableField(BaseItem): obj_type = "table-item" available_parents = ["table"] + ellide_text = "..." def __init__(self, cord_x, cord_y, value, *args, **kwargs): super(TableField, self).__init__(*args, **kwargs) From 643ad0fcde771b3d86b6077633c43e93e9277bb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:01:56 +0100 Subject: [PATCH 021/327] added import for font factory --- pype/scripts/slate/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index b6e4bd2701..1cea093929 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -3,6 +3,7 @@ import sys import re import copy import json +import collections from queue import Queue sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor From a4f2464583f30a7a9cb9f1ca94e0588c84006e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:10 +0100 Subject: [PATCH 022/327] replaced cord_x and cord_y with col_idx and row_idx --- pype/scripts/slate/base.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1cea093929..5c81d11190 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -803,20 +803,20 @@ class ItemTable(BaseItem): height -= 1 return height - def content_pos_info_by_cord(self, cord_x, cord_y): + def content_pos_info_by_cord(self, row_idx, col_idx): row_heights, col_widths = self.size_values - pos_x = self.value_pos_x - pos_y = self.value_pos_y + pos_x = int(self.value_pos_x) + pos_y = int(self.value_pos_y) width = 0 height = 0 for idx, value in enumerate(col_widths): - if cord_y == idx: + if col_idx == idx: width = value break pos_x += value for idx, value in enumerate(row_heights): - if cord_x == idx: + if row_idx == idx: height = value break pos_y += value @@ -830,10 +830,11 @@ class TableField(BaseItem): available_parents = ["table"] ellide_text = "..." - def __init__(self, cord_x, cord_y, value, *args, **kwargs): + def __init__(self, row_idx, col_idx, value, *args, **kwargs): super(TableField, self).__init__(*args, **kwargs) - self.cord_x = cord_x - self.cord_y = cord_y + self.row_idx = row_idx + self.col_idx = col_idx + self.value = value def recalculate_by_width(self, value, max_width): @@ -970,21 +971,21 @@ class TableField(BaseItem): @property def item_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) return pos_x @property def item_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) return pos_y @property def value_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) alignment_hor = self.style["alignment-horizontal"].lower() if alignment_hor in ["center", "centre"]: @@ -1003,7 +1004,7 @@ class TableField(BaseItem): @property def value_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) alignment_ver = self.style["alignment-vertical"].lower() @@ -1022,12 +1023,12 @@ class TableField(BaseItem): def draw(self, image, drawer): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) pos_start = (pos_x, pos_y) pos_end = (pos_x + width, pos_y + height) bg_color = self.style["bg-color"] - if self.parent.use_alternate_color and (self.cord_x % 2) == 1: + if self.parent.use_alternate_color and (self.row_idx % 2) == 1: bg_color = self.style["bg-alter-color"] if bg_color and bg_color.lower() != "transparent": From 3526c14c44af95c45912a6d0b481259a3811d679 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:27 +0100 Subject: [PATCH 023/327] added few changes for width and height --- pype/scripts/slate/base.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 5c81d11190..40e507209f 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -835,6 +835,13 @@ class TableField(BaseItem): self.row_idx = row_idx self.col_idx = col_idx + self.orig_value = value + + max_width = self.style.get("max-width") + max_width = self.style.get("width") or max_width + if max_width: + value = self.recalculate_by_width(value, max_width) + self.value = value def recalculate_by_width(self, value, max_width): @@ -943,6 +950,10 @@ class TableField(BaseItem): if not self.value: return 0 + width = self.style.get("width") + if width: + return int(width) + font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -952,11 +963,21 @@ class TableField(BaseItem): font_family, font_size, font_italic, font_bold ) width = font.getsize_multiline(self.value)[0] + 1 + + min_width = self.style.get("min-height") + if min_width and min_width > width: + width = min_width + return int(width) def value_height(self): if not self.value: return 0 + + height = self.style.get("height") + if height: + return int(height) + font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -966,6 +987,11 @@ class TableField(BaseItem): font_family, font_size, font_italic, font_bold ) height = font.getsize_multiline(self.value)[1] + 1 + + min_height = self.style.get("min-height") + if min_height and min_height > height: + height = min_height + return int(height) @property From 83edea59360fca36240603c9076b529ddb22a3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:50 +0100 Subject: [PATCH 024/327] remove set.json which is deprecated --- pype/scripts/slate/set.json | 59 ------------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 pype/scripts/slate/set.json diff --git a/pype/scripts/slate/set.json b/pype/scripts/slate/set.json deleted file mode 100644 index e8bba8ac41..0000000000 --- a/pype/scripts/slate/set.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "width": 1920, - "height": 1020, - "bg-color": "#000000", - "__bg-color__": "For setting constant color of background. May cause issues if not set when there are not painted spaces.", - "bg-image": null, - "__bg-image__": "May be path to static image???", - "defaults": { - "font-family": "Arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "bg-color": null, - "bg-alter-color": null, - "alignment": "left", - "__alignment[enum]__": ["left", "right", "center"] - }, - "items": [{ - "rel_pos_x": 0.1, - "rel_pos_y": 0.1, - "rel_width": 0.5, - "type": "table", - "col-format": [ - { - "font-size": 12, - "font-color": "#777777", - "alignment": "right" - }, - { - "alignment": "left" - } - ], - "rows": [ - [ - { - "name": "Version", - }, - { - "value": "mei_101_001_0020_slate_NFX_v001", - "font-bold": true - } - ], [ - { - "value": "Date:" - }, - { - "value": "{date}" - } - ] - ] - }, { - "rel_pos_x": 0.1, - "rel_pos_y": 0.1, - "rel_width": 0.5, - "rel_height": 0.5, - "type": "image", - } - ] -} From fc86e3fb19a5e0be7084e5d212aec4a9d8099f53 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:05:00 +0100 Subject: [PATCH 025/327] code cleanup --- pype/scripts/slate/base.py | 49 ++++---------------------------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 40e507209f..6376554e2a 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -1169,41 +1169,7 @@ class FontFactory: cls.fonts = available_fonts - -def main_v01(): - main_style = {"bg-color": "#777777", "margin": 0} - text_1_style = {"padding": 0, "bg-color": "#00ff77"} - text_2_style = {"padding": 0, "bg-color": "#ff0066"} - text_3_style = {"padding": 0, "bg-color": "#ff5500"} - image_1_style = {"width": 240, "height": 120, "bg-color": "#7733aa"} - table_1_style = {"padding": 0, "bg-color": "#0077ff"} - - main = MainFrame(1920, 1080, style=main_style) - layer = Layer(parent=main) - main.add_item(layer) - - text_1 = ItemText("Testing message 1", layer, text_1_style) - text_2 = ItemText("Testing 2", layer, text_2_style) - text_3 = ItemText("Testing message 3", layer, text_3_style) - - table_1_items = [["0", "Output text 1", "ha"], ["1", "Output 2"], ["2", "Output text 3"]] - table_1 = ItemTable(table_1_items, True, parent=layer, style=table_1_style) - - image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" - image_1 = ItemImage(image_1_path, layer, image_1_style) - - layer.add_item(text_1) - layer.add_item(text_2) - layer.add_item(text_3) - - layer.add_item(table_1) - layer.add_item(image_1) - - dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" - main.draw(dst) - - -def main_v02(): +def main(): cur_folder = os.path.dirname(os.path.abspath(__file__)) input_json = os.path.join(cur_folder, "netflix_v01.1.json") with open(input_json) as json_file: @@ -1219,7 +1185,6 @@ def main_v02(): for item in slate_data["items"]: load_queue.put((item, main)) - all_objs = [] while not load_queue.empty(): item_data, parent = load_queue.get() @@ -1260,23 +1225,17 @@ def main_v02(): item_obj = ItemRectangle(**kwargs) if not item_obj: + # TODO logging print( "Slate item not implemented <{}> - skipping".format(item_type) ) continue - all_objs.append(item_obj) - parent.add_item(item_obj) main.draw() - # for item in all_objs: - # print(item.style.get("width"), item.style.get("height")) - # print(item.width, item.height) - # print(item.content_pos_x, item.content_pos_y) - # print(item.value_pos_x, item.value_pos_y) + print("*** Drawing is done") if __name__ == "__main__": - main_v02() - print("*** Drawing is done") + main() From a2470faa64627036d3b8ae9f1d0e2b9b57b958bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 15:29:36 +0100 Subject: [PATCH 026/327] col row attribute name fix --- pype/scripts/slate/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 6376554e2a..457618327c 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -214,7 +214,7 @@ class BaseObj: col_idx, row_idx = get_indexes_from_regex_match( result, True ) - if self.row_idx == col_idx and self.row_idx == row_idx: + if self.col_idx == col_idx and self.row_idx == row_idx: obj_specific.update(value) output = {} From e9d2bab7910a1cca0553e218fb036356e54fdea7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 16:33:43 +0100 Subject: [PATCH 027/327] fixes for working slate --- pype/scripts/slate/base.py | 14 ++++--- pype/scripts/slate/netflix_v01.1.json | 57 +++++++++++++++++---------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 457618327c..bd090b2556 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -477,12 +477,12 @@ class Layer(BaseObj): item = _item break - if self.direction != 0: + if self.direction == 1: for id, _item in self.items.items(): if item_id == id: break - pos_x += _item.height() + pos_x += _item.width() if _item.obj_type != "image": pos_x += 1 @@ -497,6 +497,7 @@ class Layer(BaseObj): margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin pos_x += margin_left + return int(pos_x) def child_pos_y(self, item_id): @@ -537,11 +538,11 @@ class Layer(BaseObj): if height > item.height(): continue # times 1 because won't get object pointer but number - height = item.height() - else: height += item.height() + else: + height = item.height() - + # TODO this is not right min_height = self.style.get("min-height") if min_height and min_height > height: return min_height @@ -550,7 +551,7 @@ class Layer(BaseObj): def value_width(self): width = 0 for item in self.items.values(): - if self.direction == 0: + if self.direction == 1: if width > item.width(): continue # times 1 because won't get object pointer but number @@ -1201,6 +1202,7 @@ def main(): kwargs = { "parent": parent, "style": item_style, + "name": item_name, "pos_x": pos_x, "pos_y": pos_y } diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.1.json index a2be6d13e0..91058ddb50 100644 --- a/pype/scripts/slate/netflix_v01.1.json +++ b/pype/scripts/slate/netflix_v01.1.json @@ -15,6 +15,10 @@ "padding": 0, "margin": 0 }, + "layer": { + "padding": 0, + "margin": 0 + }, "rectangle": { "padding": 0, "margin": 0, @@ -37,9 +41,9 @@ "padding-bottom": 10, "alignment-vertical": "top", "alignment-horizontal": "left", - "word-wrap": true, + "word-wrap": false, "ellide": true, - "max-lines": 3 + "max-lines": 1 }, "table-item-col[0]": { "font-size": 20, @@ -60,20 +64,30 @@ "height": 1000 }, "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], + "type": "layer", + "direction": 1, "style": { + "layer": {"padding": 0}, "table-item-col[0]": { - "width": 300 + "width": 200 } - } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ] + }, { + "type": "table", + "values": [ + ["Submitting For:", "SAMPLE"] + ] + }] }, { "type": "rectangle", "style": { "bg-color": "#d40914", - "width": 1094, + "width": 1108, "height": 5, "fill": true } @@ -87,6 +101,11 @@ ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] ], "style": { + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, "table-item-col[0]": { "alignment-horizontal": "right", "width": 300 @@ -100,24 +119,20 @@ }, { "type": "layer", "name": "RightSide", - "pos_x": 1174, - "pos_y": 40, - "style": { - "width": 733, - "height": 1000 - }, + "pos_x": 1164, + "pos_y": 26, "items": [{ "type": "image", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", "style": { - "width": 733, - "height": 414 + "width": 730, + "height": 412 } }, { "type": "image", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", "style": { - "width": 733, + "width": 730, "height": 55 } }, { @@ -130,10 +145,12 @@ ], "style": { "table-item-col[0]": { - "alignment-horizontal": "left" + "alignment-horizontal": "left", + "width": 200 }, "table-item-col[1]": { - "alignment-horizontal": "right" + "alignment-horizontal": "right", + "width": 530 } } }] From db1e578cd80bdda0f89a11addf64bd748fd55057 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 18:48:38 +0100 Subject: [PATCH 028/327] fixed cases when value of tablefield does not need to ellide or word wrap --- pype/scripts/slate/base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index bd090b2556..cb72d9af1c 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -853,6 +853,18 @@ class TableField(BaseItem): ellide = self.style.get("ellide") max_lines = self.style.get("max-lines") + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + val_width = font.getsize(value)[0] + if val_width <= max_width: + return value + if not ellide and not word_wrap: # TODO logging print(( From 9cc2c4d5844e4442256bee204286cb475ce5174e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 18:52:43 +0100 Subject: [PATCH 029/327] few padding fixes --- pype/scripts/slate/base.py | 65 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index cb72d9af1c..a5404f318d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -261,7 +261,10 @@ class BaseObj: def value_pos_x(self): pos_x = int(self.content_pos_x) padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + pos_x += padding_left return pos_x @@ -270,7 +273,10 @@ class BaseObj: def value_pos_y(self): pos_y = int(self.content_pos_y) padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + pos_y += padding_top return pos_y @@ -314,15 +320,27 @@ class BaseObj: def content_width(self): width = self.value_width() padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + return width + padding_left + padding_right def content_height(self): height = self.value_height() padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + padding_bottom = self.style.get("padding-bottom") + if padding_bottom is None: + padding_bottom = padding + return height + padding_top + padding_bottom def width(self): @@ -343,30 +361,6 @@ class BaseObj: return height + margin_bottom + margin_top - # @property - # def max_width(self): - # return self.style.get("max-width") or self.width - # - # @property - # def max_height(self): - # return self.style.get("max-height") or self.height - # - # @property - # def max_content_width(self): - # width = self.max_width - # padding = self.style["padding"] - # padding_left = self.style.get("padding-left") or padding - # padding_right = self.style.get("padding-right") or padding - # return (width - (padding_left + padding_right)) - # - # @property - # def max_content_height(self): - # height = self.max_height - # padding = self.style["padding"] - # padding_top = self.style.get("padding-top") or padding - # padding_bottom = self.style.get("padding-bottom") or padding - # return (height - (padding_top + padding_bottom)) - def add_item(self, item): self.items[item.id] = item @@ -846,6 +840,17 @@ class TableField(BaseItem): self.value = value def recalculate_by_width(self, value, max_width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + + max_width -= (padding_left + padding_right) + if not value: return "" From ac202b9fbd3fbe1eb3cbf179a06655723f6dc0ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 14:28:20 +0100 Subject: [PATCH 030/327] implemented placeholder --- pype/scripts/slate/base.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index a5404f318d..4fc7cdb4d9 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -646,6 +646,53 @@ class ItemRectangle(BaseItem): return int(self.style["height"]) +class ItemPlaceHolder(BaseItem): + obj_type = "placeholder" + + def __init__(self, image_path, *args, **kwargs): + self.image_path = image_path + super(ItemPlaceHolder, self).__init__(*args, **kwargs) + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + + kwargs = {} + if bg_color != "tranparent": + kwargs["fill"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + def collect_data(self): + return { + "pos_x": self.value_pos_x, + "pos_y": self.value_pos_y, + "width": self.value_width(), + "height": self.value_height(), + "path": self.image_path + } + + class ItemText(BaseItem): obj_type = "text" From 4a8ab6fac8dbe9137304d1be036f8e01ebf32846 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 14:29:19 +0100 Subject: [PATCH 031/327] added collect_data and fill_data abilities --- pype/scripts/slate/base.py | 382 ++++++++++++------ .../{netflix_v01.1.json => netflix_v01.json} | 87 +++- pype/scripts/slate/netflix_v02.json | 213 ++++++++++ pype/scripts/slate/netflix_v03.json | 213 ++++++++++ 4 files changed, 758 insertions(+), 137 deletions(-) rename pype/scripts/slate/{netflix_v01.1.json => netflix_v01.json} (65%) create mode 100644 pype/scripts/slate/netflix_v02.json create mode 100644 pype/scripts/slate/netflix_v03.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 4fc7cdb4d9..1f90f92992 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -25,6 +25,7 @@ class BaseObj: "margin-top", "margin-bottom", "width", "height", "fill" ] + fill_data_regex = r"{[^}]+}" def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): if not self.obj_type: @@ -56,6 +57,16 @@ class BaseObj: self._pos_x = pos_x or 0 self._pos_y = pos_y or 0 + if parent: + parent.add_item(self) + + def fill_data_format(self): + return + + @property + def fill_data(self): + return self.parent.fill_data + @property def main_style(self): default_style_v1 = { @@ -66,40 +77,43 @@ class BaseObj: "font-bold": False, "font-italic": False, "bg-color": "#0077ff", - "bg-alter-color": "#0055dd", - "alignment-horizontal": "center", - "alignment-vertical": "bottom", - "padding": 0, - "margin": 0, - }, - "main_frame": { - "padding": 0, - "margin": 0 + "alignment-horizontal": "left", + "alignment-vertical": "top" }, "layer": { "padding": 0, "margin": 0 }, - "image": { + "rectangle": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "#E9324B", + "fill": true }, - "text": { + "main_frame": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "#252525" }, "table": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "transparent" }, "table-item": { - "alignment-horizontal": "right", - "padding": 0, - "margin": 0 - }, - "__not_implemented__": { - "table-item-col-0": {}, - "#MyName": {} + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 } } return default_style_v1 @@ -118,6 +132,31 @@ class BaseObj: ) ) + def collect_data(self): + return None + + def find_item(self, obj_type=None, name=None): + obj_type_fits = False + name_fits = False + if obj_type is None or self.obj_type == obj_type: + obj_type_fits = True + + if name is None or self.name != name: + name_fits = True + + output = [] + if obj_type_fits and name_fits: + output.append(self) + + if not self.items: + return output + + for item in self.items.values(): + output.extend( + item.find_item(obj_type=obj_type, name=name) + ) + return output + @property def full_style(self): if self.parent is not None: @@ -151,10 +190,9 @@ class BaseObj: if self.name: name = str(self.name) if not name.startswith("#"): - name += "#" + name = "#" + name name_specific = style.get(name) or {} - if obj_type == "table-item": col_regex = r"table-item-col\[([\d\-, ]+)*\]" row_regex = r"table-item-row\[([\d\-, ]+)*\]" @@ -363,6 +401,8 @@ class BaseObj: def add_item(self, item): self.items[item.id] = item + item.fill_data_format() + def reset(self): for item in self.items.values(): @@ -374,12 +414,24 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, destination_path=None, *args, **kwargs): + def __init__( + self, width, height, destination_path, fill_data={}, *args, **kwargs + ): kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self.dst_path = destination_path + self._fill_data = fill_data + self.fill_data_format() + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.dst_path): + self.dst_path = self.dst_path.format(**self.fill_data) + + @property + def fill_data(self): + return self._fill_data def value_width(self): width = 0 @@ -400,16 +452,7 @@ class MainFrame(BaseObj): return self._height def draw(self, path=None): - if not path: - path = self.dst_path - - if not path: - raise TypeError(( - "draw() missing 1 required positional argument: 'path'" - " if 'destination_path is not specified'" - )) - - dir_path = os.path.dirname(path) + dir_path = os.path.dirname(self.dst_path) if not os.path.exists(dir_path): os.makedirs(dir_path) @@ -419,9 +462,24 @@ class MainFrame(BaseObj): for item in self.items.values(): item.draw(image, drawer) - image.save(path) + image.save(self.dst_path) self.reset() + def collect_data(self): + output = {} + output["width"] = self.width() + output["height"] = self.height() + output["slate_path"] = self.dst_path + + placeholders = self.find_item(obj_type="placeholder") + placeholders_data = [] + for placeholder in placeholders: + placeholders_data.append(placeholder.collect_data()) + + output["placeholders"] = placeholders_data + + return output + class Layer(BaseObj): obj_type = "layer" @@ -450,6 +508,7 @@ class Layer(BaseObj): pos_y = self._pos_y else: pos_y = self.parent.value_pos_y + return int(pos_y) @property @@ -477,7 +536,7 @@ class Layer(BaseObj): break pos_x += _item.width() - if _item.obj_type != "image": + if _item.obj_type not in ["image", "placeholder"]: pos_x += 1 else: @@ -509,7 +568,7 @@ class Layer(BaseObj): if item_id == id: break pos_y += item.height() - if item.obj_type != "image": + if item.obj_type not in ["image", "placeholder"]: pos_y += 1 else: @@ -519,22 +578,18 @@ class Layer(BaseObj): elif alignment_ver == "bottom": pos_y += self.content_height() - item.content_height() - else: - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - pos_y += margin_top return int(pos_y) def value_height(self): height = 0 for item in self.items.values(): - if self.direction == 0: + if self.direction == 1: if height > item.height(): continue # times 1 because won't get object pointer but number - height += item.height() - else: height = item.height() + else: + height += item.height() # TODO this is not right min_height = self.style.get("min-height") @@ -545,12 +600,14 @@ class Layer(BaseObj): def value_width(self): width = 0 for item in self.items.values(): - if self.direction == 1: + if self.direction == 0: if width > item.width(): continue # times 1 because won't get object pointer but number width = item.width() else: + if self.name == "LeftSide": + print(item, item.width()) width += item.width() min_width = self.style.get("min-width") @@ -566,7 +623,6 @@ class Layer(BaseObj): class BaseItem(BaseObj): available_parents = ["main_frame", "layer"] - @property def item_pos_x(self): if self.parent.obj_type == "main_frame": @@ -597,6 +653,10 @@ class ItemImage(BaseItem): super(ItemImage, self).__init__(*args, **kwargs) self.image_path = image_path + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + def draw(self, image, drawer): source_image = Image.open(os.path.normpath(self.image_path)) paste_image = source_image.resize( @@ -756,15 +816,25 @@ class ItemTable(BaseItem): obj_type = "table" def __init__(self, values, use_alternate_color=False, *args, **kwargs): + + self.values_by_cords = None + self.prepare_values(values) + super(ItemTable, self).__init__(*args, **kwargs) self.size_values = None - self.values_by_cords = None - - self.prepare_values(values) self.calculate_sizes() self.use_alternate_color = use_alternate_color + def add_item(self, item): + if item.obj_type == "table-item": + return + super(ItemTable, self).add_item(item) + + def fill_data_format(self): + for item in self.values: + item.fill_data_format() + def prepare_values(self, _values): values = [] values_by_cords = [] @@ -876,14 +946,6 @@ class TableField(BaseItem): super(TableField, self).__init__(*args, **kwargs) self.row_idx = row_idx self.col_idx = col_idx - - self.orig_value = value - - max_width = self.style.get("max-width") - max_width = self.style.get("width") or max_width - if max_width: - value = self.recalculate_by_width(value, max_width) - self.value = value def recalculate_by_width(self, value, max_width): @@ -928,24 +990,23 @@ class TableField(BaseItem): elif ellide and not word_wrap: max_lines = 1 - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - words = [word for word in value.split()] words_len = len(words) lines = [] - last_index = 0 + last_index = None while True: + start_index = 0 + if last_index is not None: + start_index = int(last_index) + 1 + line = "" - for idx in range(last_index, words_len): + for idx in range(start_index, words_len): _word = words[idx] - _line = " ".join([line, _word]) + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word]) _line_width = font.getsize(_line)[0] if _line_width > max_width: break @@ -958,7 +1019,7 @@ class TableField(BaseItem): if last_index == words_len - 1: break - elif last_index == 0: + elif last_index is None: if ellide: line = "" for idx, char in enumerate(words[idx]): @@ -968,7 +1029,7 @@ class TableField(BaseItem): if idx == 0: line = _line break - line = _line + line = line + char lines.append(line) # TODO logging @@ -979,46 +1040,103 @@ class TableField(BaseItem): if not lines: return output - if max_lines and len(lines) > max_lines: - lines = [lines[idx] for idx in range(max_lines)] - if not ellide: - return "\n".join(lines) + over_max_lines = (max_lines and len(lines) > max_lines) + if not over_max_lines: + return "\n".join([line for line in lines]) - last_line = lines[-1] - last_line_width = font.getsize(last_line + self.ellide_text)[0] - if last_line_width <= max_width: - lines[-1] += self.ellide_text + lines = [lines[idx] for idx in range(max_lines)] + if not ellide: + return "\n".join(lines) + + last_line = lines[-1] + last_line_width = font.getsize(last_line + self.ellide_text)[0] + if last_line_width <= max_width: + lines[-1] += self.ellide_text + return "\n".join([line for line in lines]) + + last_line_words = last_line.split() + if len(last_line_words) == 1: + if max_lines > 1: + # TODO try previous line? + lines[-1] = self.ellide_text return "\n".join([line for line in lines]) - last_line_words = last_line.split() - if len(last_line_words) == 1: - if max_lines > 1: - lines[-1] = self.ellide_text - return "\n".join([line for line in lines]) + line = "" + for idx, word in enumerate(last_line_words): + _line = line + word + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + lines[-1] = line - _line = "" - for idx, char in enumerate(last_line): - _line = line + char + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = _line - lines[-1] = line - return "\n".join([line for line in lines]) + return "\n".join([line for line in lines]) + + line = "" + for idx, _word in enumerate(last_line_words): + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word + self.ellide_text]) + _line_width = font.getsize(_line)[0] + + if _line_width <= max_width: + line = connector.join([line, _word]) + continue + + if idx != 0: + line += self.ellide_text + break + + if max_lines != 1: + # TODO try previous line? + line = self.ellide_text + break + + for idx, char in enumerate(_word): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = line + char + break + + lines[-1] = line return "\n".join([line for line in lines]) + def fill_data_format(self): + value = self.value + if re.match(self.fill_data_regex, value): + value = value.format(**self.fill_data) + + self.orig_value = value + + max_width = self.style.get("max-width") + max_width = self.style.get("width") or max_width + if max_width: + value = self.recalculate_by_width(value, max_width) + + self.value = value + + def content_width(self): + width = self.style.get("width") + if width: + return int(width) + return super(TableField, self).content_width() + + def content_height(self): + return super(TableField, self).content_height() def value_width(self): if not self.value: return 0 - width = self.style.get("width") - if width: - return int(width) - font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -1087,7 +1205,10 @@ class TableField(BaseItem): else: padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + pos_x += padding_left return int(pos_x) @@ -1107,7 +1228,10 @@ class TableField(BaseItem): else: padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + pos_y += padding_top return int(pos_y) @@ -1139,11 +1263,17 @@ class TableField(BaseItem): font = FontFactory.get_font( font_family, font_size, font_italic, font_bold ) - drawer.text( + + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor == "centre": + alignment_hor = "center" + + drawer.multiline_text( self.value_pos_start, self.value, font=font, - fill=font_color + fill=font_color, + align=alignment_hor ) @@ -1236,15 +1366,36 @@ class FontFactory: def main(): cur_folder = os.path.dirname(os.path.abspath(__file__)) - input_json = os.path.join(cur_folder, "netflix_v01.1.json") + # input_json = os.path.join(cur_folder, "netflix_v01.json") + # input_json = os.path.join(cur_folder, "netflix_v02.json") + input_json = os.path.join(cur_folder, "netflix_v03.json") with open(input_json) as json_file: slate_data = json.load(json_file) + fill_data = { + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "project": { + "name": "Project name" + }, + "intent": "WIP", + "version_name": "mei_101_001_0020_slate_NFX_v001", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", + "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "vendor": "DAZZLE", + "shot_name": "SLATE_SIMPLE", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } width = slate_data["width"] height = slate_data["height"] + dst_path = slate_data["destination_path"] style = slate_data.get("style") or {} - dst_path = slate_data.get("destination_path") - main = MainFrame(width, height, destination_path=dst_path, style=style) + + main = MainFrame(width, height, dst_path, fill_data, style=style) load_queue = Queue() for item in slate_data["items"]: @@ -1271,7 +1422,6 @@ def main(): "pos_y": pos_y } - item_obj = None if item_type == "layer": direction = item_data.get("direction", 0) item_obj = Layer(direction, **kwargs) @@ -1281,26 +1431,28 @@ def main(): elif item_type == "table": use_alternate_color = item_data.get("use_alternate_color", False) values = item_data.get("values") or [] - item_obj = ItemTable(values, use_alternate_color, **kwargs) + ItemTable(values, use_alternate_color, **kwargs) elif item_type == "image": path = item_data["path"] - item_obj = ItemImage(path, **kwargs) + ItemImage(path, **kwargs) elif item_type == "rectangle": - item_obj = ItemRectangle(**kwargs) + ItemRectangle(**kwargs) - if not item_obj: + elif item_type == "placeholder": + path = item_data["path"] + ItemPlaceHolder(path, **kwargs) + + else: # TODO logging print( "Slate item not implemented <{}> - skipping".format(item_type) ) - continue - - parent.add_item(item_obj) main.draw() - print("*** Drawing is done") + print(main.collect_data()) + print("*** Finished") if __name__ == "__main__": diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.json similarity index 65% rename from pype/scripts/slate/netflix_v01.1.json rename to pype/scripts/slate/netflix_v01.json index 91058ddb50..9a5974839d 100644 --- a/pype/scripts/slate/netflix_v01.1.json +++ b/pype/scripts/slate/netflix_v01.json @@ -1,7 +1,7 @@ { "width": 1920, "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v001.png", + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v01.png", "style": { "*": { "font-family": "arial", @@ -32,33 +32,40 @@ "bg-color": "transparent" }, "table-item": { - "bg-color": "#272727", - "bg-alter-color": "#212121", - "font-color": "#ffffff", - "font-bold": true, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, "font-italic": false, "padding": 5, "padding-bottom": 10, - "alignment-vertical": "top", "alignment-horizontal": "left", + "alignment-vertical": "top", "word-wrap": false, "ellide": true, "max-lines": 1 }, "table-item-col[0]": { "font-size": 20, - "font-color": "#898989" + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null }, "table-item-col[1]": { - "font-size": 30, - "padding-left": 20 + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" } }, "items": [{ "type": "layer", "name": "LeftSide", - "pos_x": 40, - "pos_y": 40, + "pos_x": 27, + "pos_y": 27, "style": { "width": 1094, "height": 1000 @@ -67,26 +74,52 @@ "type": "layer", "direction": 1, "style": { - "layer": {"padding": 0}, + "table-item": { + "bg-color": "transparent" + }, "table-item-col[0]": { - "width": 200 + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 } }, "items": [{ "type": "table", "values": [ ["Show:", "First Contact"] - ] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } }, { "type": "table", "values": [ ["Submitting For:", "SAMPLE"] - ] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } }] }, { "type": "rectangle", "style": { - "bg-color": "#d40914", + "bg-color": "#bc1015", "width": 1108, "height": 5, "fill": true @@ -101,18 +134,24 @@ ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] ], "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, "table-item-field[1:3]": { "word-wrap": true, "ellide": true, - "max-lines": 4 + "max-lines": 2 }, "table-item-col[0]": { "alignment-horizontal": "right", - "width": 300 + "width": 150 }, "table-item-col[1]": { "alignment-horizontal": "left", - "width": 786 + "width": 958 } } }] @@ -122,15 +161,18 @@ "pos_x": 1164, "pos_y": 26, "items": [{ - "type": "image", + "type": "placeholder", + "name": "thumbnail", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", "style": { "width": 730, "height": 412 } }, { - "type": "image", + "type": "placeholder", + "name": "colorbar", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "return_data": true, "style": { "width": 730, "height": 55 @@ -150,7 +192,8 @@ }, "table-item-col[1]": { "alignment-horizontal": "right", - "width": 530 + "width": 530, + "font-size": 30 } } }] diff --git a/pype/scripts/slate/netflix_v02.json b/pype/scripts/slate/netflix_v02.json new file mode 100644 index 0000000000..f373ed8134 --- /dev/null +++ b/pype/scripts/slate/netflix_v02.json @@ -0,0 +1,213 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v02.png", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "SAMPLE"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "mei_101_001_0020_slate_NFX_v001"], + ["Date:", "2019-08-09"], + ["Shot Types:", "2d comp"], + ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 2 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "DAZZLE"], + ["Shot Name:", "SLATE_SIMPLE"], + ["Frames:", "0 - 1 (2)"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} diff --git a/pype/scripts/slate/netflix_v03.json b/pype/scripts/slate/netflix_v03.json new file mode 100644 index 0000000000..975cb60133 --- /dev/null +++ b/pype/scripts/slate/netflix_v03.json @@ -0,0 +1,213 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} From 6ef5853b65cb86cbdd94ae8d713ddb1fac753bdf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 16:33:15 +0100 Subject: [PATCH 032/327] reorganized slates a little bit --- pype/scripts/slate/base.py | 1459 ----------------- pype/scripts/slate/default_style.json | 44 - pype/scripts/slate/lib.py | 114 ++ pype/scripts/slate/slate.py | 355 ---- pype/scripts/slate/slate_base/__init__.py | 0 pype/scripts/slate/slate_base/base.py | 373 +++++ .../slate/slate_base/default_style.json | 58 + pype/scripts/slate/slate_base/font_factory.py | 93 ++ pype/scripts/slate/slate_base/items.py | 666 ++++++++ pype/scripts/slate/slate_base/layer.py | 139 ++ pype/scripts/slate/slate_base/main_frame.py | 77 + pype/scripts/slate/style_schema.json | 44 - 12 files changed, 1520 insertions(+), 1902 deletions(-) delete mode 100644 pype/scripts/slate/base.py delete mode 100644 pype/scripts/slate/default_style.json create mode 100644 pype/scripts/slate/lib.py delete mode 100644 pype/scripts/slate/slate.py create mode 100644 pype/scripts/slate/slate_base/__init__.py create mode 100644 pype/scripts/slate/slate_base/base.py create mode 100644 pype/scripts/slate/slate_base/default_style.json create mode 100644 pype/scripts/slate/slate_base/font_factory.py create mode 100644 pype/scripts/slate/slate_base/items.py create mode 100644 pype/scripts/slate/slate_base/layer.py create mode 100644 pype/scripts/slate/slate_base/main_frame.py delete mode 100644 pype/scripts/slate/style_schema.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py deleted file mode 100644 index 1f90f92992..0000000000 --- a/pype/scripts/slate/base.py +++ /dev/null @@ -1,1459 +0,0 @@ -import os -import sys -import re -import copy -import json -import collections -from queue import Queue -sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") -from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor -from uuid import uuid4 - - -class BaseObj: - """Base Object for slates.""" - - obj_type = None - available_parents = [] - all_style_keys = [ - "font-family", "font-size", "font-color", "font-bold", "font-italic", - "bg-color", "bg-alter-color", - "alignment-horizontal", "alignment-vertical", - "padding", "padding-left", "padding-right", - "padding-top", "padding-bottom", - "margin", "margin-left", "margin-right", - "margin-top", "margin-bottom", "width", "height", - "fill" - ] - fill_data_regex = r"{[^}]+}" - - def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): - if not self.obj_type: - raise NotImplementedError( - "Class don't have set object type <{}>".format( - self.__class__.__name__ - ) - ) - - parent_obj_type = None - if parent: - parent_obj_type = parent.obj_type - - if parent_obj_type not in self.available_parents: - expected_parents = ", ".join(self.available_parents) - raise Exception(( - "Invalid parent <{}> for <{}>. Expected <{}>" - ).format( - parent.__class__.__name__, self.obj_type, expected_parents - )) - - self.parent = parent - self._style = style - - self.id = uuid4() - self.name = name - self.items = {} - - self._pos_x = pos_x or 0 - self._pos_y = pos_y or 0 - - if parent: - parent.add_item(self) - - def fill_data_format(self): - return - - @property - def fill_data(self): - return self.parent.fill_data - - @property - def main_style(self): - default_style_v1 = { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": False, - "font-italic": False, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - } - } - return default_style_v1 - - def height(self): - raise NotImplementedError( - "Attribute `height` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - def width(self): - raise NotImplementedError( - "Attribute `width` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - def collect_data(self): - return None - - def find_item(self, obj_type=None, name=None): - obj_type_fits = False - name_fits = False - if obj_type is None or self.obj_type == obj_type: - obj_type_fits = True - - if name is None or self.name != name: - name_fits = True - - output = [] - if obj_type_fits and name_fits: - output.append(self) - - if not self.items: - return output - - for item in self.items.values(): - output.extend( - item.find_item(obj_type=obj_type, name=name) - ) - return output - - @property - def full_style(self): - if self.parent is not None: - style = dict(val for val in self.parent.full_style.items()) - else: - style = self.main_style - - for key, value in self._style.items(): - if key in self.all_style_keys: - # TODO which variant is right? - style[self.obj_type][key] = value - # style["*"][key] = value - else: - if key not in style: - style[key] = {} - - if isinstance(style[key], dict): - style[key].update(value) - else: - style[key] = value - - return style - - def get_style_for_obj_type(self, obj_type, style=None): - if not style: - style = copy.deepcopy(self.full_style) - - base = style.get("*") or {} - obj_specific = style.get(obj_type) or {} - name_specific = {} - if self.name: - name = str(self.name) - if not name.startswith("#"): - name = "#" + name - name_specific = style.get(name) or {} - - if obj_type == "table-item": - col_regex = r"table-item-col\[([\d\-, ]+)*\]" - row_regex = r"table-item-row\[([\d\-, ]+)*\]" - field_regex = ( - r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" - ) - # STRICT field regex (not allowed spaces) - # fild_regex = r"table-item-field\[(\d+:\d+)*\]" - - def get_indexes_from_regex_match(result, field=False): - group = result.group(1) - indexes = [] - if field: - return [ - int(part.strip()) for part in group.strip().split(":") - ] - - parts = group.strip().split(",") - for part in parts: - part = part.strip() - if "-" not in part: - indexes.append(int(part)) - continue - - sub_parts = [ - int(sub.strip()) for sub in part.split("-") - ] - if len(sub_parts) != 2: - # TODO logging - print("invalid range '{}'".format(part)) - continue - - for idx in range(sub_parts[0], sub_parts[1]+1): - indexes.append(idx) - return indexes - - for key, value in style.items(): - if not key.startswith(obj_type): - continue - - result = re.search(col_regex, key) - if result: - indexes = get_indexes_from_regex_match(result) - if self.col_idx in indexes: - obj_specific.update(value) - continue - - result = re.search(row_regex, key) - if result: - indexes = get_indexes_from_regex_match(result) - if self.row_idx in indexes: - obj_specific.update(value) - continue - - result = re.search(field_regex, key) - if result: - col_idx, row_idx = get_indexes_from_regex_match( - result, True - ) - if self.col_idx == col_idx and self.row_idx == row_idx: - obj_specific.update(value) - - output = {} - output.update(base) - output.update(obj_specific) - output.update(name_specific) - - return output - - @property - def style(self): - return self.get_style_for_obj_type(self.obj_type) - - @property - def item_pos_x(self): - if self.parent.obj_type == "main_frame": - return int(self._pos_x) - return 0 - - @property - def item_pos_y(self): - if self.parent.obj_type == "main_frame": - return int(self._pos_y) - return 0 - - @property - def content_pos_x(self): - pos_x = self.item_pos_x - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - pos_x += margin_left - - return pos_x - - @property - def content_pos_y(self): - pos_y = self.item_pos_y - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - return pos_y + margin_top - - @property - def value_pos_x(self): - pos_x = int(self.content_pos_x) - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - pos_x += padding_left - - return pos_x - - @property - def value_pos_y(self): - pos_y = int(self.content_pos_y) - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - pos_y += padding_top - - return pos_y - - @property - def value_pos_start(self): - return (self.value_pos_x, self.value_pos_y) - - @property - def value_pos_end(self): - pos_x, pos_y = self.value_pos_start - pos_x += self.width() - pos_y += self.height() - return (pos_x, pos_y) - - @property - def content_pos_start(self): - return (self.content_pos_x, self.content_pos_y) - - @property - def content_pos_end(self): - pos_x, pos_y = self.content_pos_start - pos_x += self.content_width() - pos_y += self.content_height() - return (pos_x, pos_y) - - def value_width(self): - raise NotImplementedError( - "Attribute is not implemented <{}>".format( - self.__class__.__name__ - ) - ) - - def value_height(self): - raise NotImplementedError( - "Attribute is not implemented for <{}>".format( - self.__class__.__name__ - ) - ) - - def content_width(self): - width = self.value_width() - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - padding_right = self.style.get("padding-right") - if padding_right is None: - padding_right = padding - - return width + padding_left + padding_right - - def content_height(self): - height = self.value_height() - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - padding_bottom = self.style.get("padding-bottom") - if padding_bottom is None: - padding_bottom = padding - - return height + padding_top + padding_bottom - - def width(self): - width = self.content_width() - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - return width + margin_left + margin_right - - def height(self): - height = self.content_height() - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - return height + margin_bottom + margin_top - - def add_item(self, item): - self.items[item.id] = item - item.fill_data_format() - - - def reset(self): - for item in self.items.values(): - item.reset() - - -class MainFrame(BaseObj): - - obj_type = "main_frame" - available_parents = [None] - - def __init__( - self, width, height, destination_path, fill_data={}, *args, **kwargs - ): - kwargs["parent"] = None - super(MainFrame, self).__init__(*args, **kwargs) - self._width = width - self._height = height - self.dst_path = destination_path - self._fill_data = fill_data - self.fill_data_format() - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.dst_path): - self.dst_path = self.dst_path.format(**self.fill_data) - - @property - def fill_data(self): - return self._fill_data - - def value_width(self): - width = 0 - for item in self.items.values(): - width += item.width() - return width - - def value_height(self): - height = 0 - for item in self.items.values(): - height += item.height() - return height - - def width(self): - return self._width - - def height(self): - return self._height - - def draw(self, path=None): - dir_path = os.path.dirname(self.dst_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width(), self.height()), color=bg_color) - drawer = ImageDraw.Draw(image) - for item in self.items.values(): - item.draw(image, drawer) - - image.save(self.dst_path) - self.reset() - - def collect_data(self): - output = {} - output["width"] = self.width() - output["height"] = self.height() - output["slate_path"] = self.dst_path - - placeholders = self.find_item(obj_type="placeholder") - placeholders_data = [] - for placeholder in placeholders: - placeholders_data.append(placeholder.collect_data()) - - output["placeholders"] = placeholders_data - - return output - - -class Layer(BaseObj): - obj_type = "layer" - available_parents = ["main_frame", "layer"] - - # Direction can be 0=vertical/ 1=horizontal - def __init__(self, direction=0, *args, **kwargs): - super(Layer, self).__init__(*args, **kwargs) - self._direction = direction - - @property - def item_pos_x(self): - if self.parent.obj_type == self.obj_type: - pos_x = self.parent.child_pos_x(self.id) - elif self.parent.obj_type == "main_frame": - pos_x = self._pos_x - else: - pos_x = self.parent.value_pos_x - return int(pos_x) - - @property - def item_pos_y(self): - if self.parent.obj_type == self.obj_type: - pos_y = self.parent.child_pos_y(self.id) - elif self.parent.obj_type == "main_frame": - pos_y = self._pos_y - else: - pos_y = self.parent.value_pos_y - - return int(pos_y) - - @property - def direction(self): - if self._direction not in (0, 1): - print( - "Direction must be 0 or 1 (0 is horizontal / 1 is vertical)!" - ) - return 0 - return self._direction - - def child_pos_x(self, item_id): - pos_x = self.value_pos_x - alignment_hor = self.style["alignment-horizontal"].lower() - - item = None - for id, _item in self.items.items(): - if item_id == id: - item = _item - break - - if self.direction == 1: - for id, _item in self.items.items(): - if item_id == id: - break - - pos_x += _item.width() - if _item.obj_type not in ["image", "placeholder"]: - pos_x += 1 - - else: - if alignment_hor in ["center", "centre"]: - pos_x += (self.content_width() - item.content_width()) / 2 - - elif alignment_hor == "right": - pos_x += self.content_width() - item.content_width() - - else: - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - pos_x += margin_left - - return int(pos_x) - - def child_pos_y(self, item_id): - pos_y = self.value_pos_y - alignment_ver = self.style["alignment-horizontal"].lower() - - item = None - for id, _item in self.items.items(): - if item_id == id: - item = _item - break - - if self.direction != 1: - for id, item in self.items.items(): - if item_id == id: - break - pos_y += item.height() - if item.obj_type not in ["image", "placeholder"]: - pos_y += 1 - - else: - if alignment_ver in ["center", "centre"]: - pos_y += (self.content_height() - item.content_height()) / 2 - - elif alignment_ver == "bottom": - pos_y += self.content_height() - item.content_height() - - return int(pos_y) - - def value_height(self): - height = 0 - for item in self.items.values(): - if self.direction == 1: - if height > item.height(): - continue - # times 1 because won't get object pointer but number - height = item.height() - else: - height += item.height() - - # TODO this is not right - min_height = self.style.get("min-height") - if min_height and min_height > height: - return min_height - return height - - def value_width(self): - width = 0 - for item in self.items.values(): - if self.direction == 0: - if width > item.width(): - continue - # times 1 because won't get object pointer but number - width = item.width() - else: - if self.name == "LeftSide": - print(item, item.width()) - width += item.width() - - min_width = self.style.get("min-width") - if min_width and min_width > width: - return min_width - return width - - def draw(self, image, drawer): - for item in self.items.values(): - item.draw(image, drawer) - - -class BaseItem(BaseObj): - available_parents = ["main_frame", "layer"] - - @property - def item_pos_x(self): - if self.parent.obj_type == "main_frame": - return self._pos_x - return self.parent.child_pos_x(self.id) - - @property - def item_pos_y(self): - if self.parent.obj_type == "main_frame": - return self._pos_y - return self.parent.child_pos_y(self.id) - - def add_item(self, *args, **kwargs): - raise Exception("Can't add item to an item, use layers instead.") - - def draw(self, image, drawer): - raise NotImplementedError( - "Method `draw` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - -class ItemImage(BaseItem): - obj_type = "image" - - def __init__(self, image_path, *args, **kwargs): - super(ItemImage, self).__init__(*args, **kwargs) - self.image_path = image_path - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.image_path): - self.image_path = self.image_path.format(**self.fill_data) - - def draw(self, image, drawer): - source_image = Image.open(os.path.normpath(self.image_path)) - paste_image = source_image.resize( - (self.value_width(), self.value_height()), - Image.ANTIALIAS - ) - image.paste( - paste_image, - (self.value_pos_x, self.value_pos_y) - ) - - def value_width(self): - return int(self.style["width"]) - - def value_height(self): - return int(self.style["height"]) - - -class ItemRectangle(BaseItem): - obj_type = "rectangle" - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - fill = self.style.get("fill", False) - kwargs = {} - if fill: - kwargs["fill"] = bg_color - else: - kwargs["outline"] = bg_color - - start_pos_x = self.value_pos_x - start_pos_y = self.value_pos_y - end_pos_x = start_pos_x + self.value_width() - end_pos_y = start_pos_y + self.value_height() - drawer.rectangle( - ( - (start_pos_x, start_pos_y), - (end_pos_x, end_pos_y) - ), - **kwargs - ) - - def value_width(self): - return int(self.style["width"]) - - def value_height(self): - return int(self.style["height"]) - - -class ItemPlaceHolder(BaseItem): - obj_type = "placeholder" - - def __init__(self, image_path, *args, **kwargs): - self.image_path = image_path - super(ItemPlaceHolder, self).__init__(*args, **kwargs) - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.image_path): - self.image_path = self.image_path.format(**self.fill_data) - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - - kwargs = {} - if bg_color != "tranparent": - kwargs["fill"] = bg_color - - start_pos_x = self.value_pos_x - start_pos_y = self.value_pos_y - end_pos_x = start_pos_x + self.value_width() - end_pos_y = start_pos_y + self.value_height() - - drawer.rectangle( - ( - (start_pos_x, start_pos_y), - (end_pos_x, end_pos_y) - ), - **kwargs - ) - - def value_width(self): - return int(self.style["width"]) - - def value_height(self): - return int(self.style["height"]) - - def collect_data(self): - return { - "pos_x": self.value_pos_x, - "pos_y": self.value_pos_y, - "width": self.value_width(), - "height": self.value_height(), - "path": self.image_path - } - - -class ItemText(BaseItem): - obj_type = "text" - - def __init__(self, value, *args, **kwargs): - super(ItemText, self).__init__(*args, **kwargs) - self.value = value - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - # TODO border outline styles - drawer.rectangle( - (self.content_pos_start, self.content_pos_end), - fill=bg_color, - outline=None - ) - - font_color = self.style["font-color"] - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - drawer.text( - self.value_pos_start, - self.value, - font=font, - fill=font_color - ) - - def value_width(self): - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - width = font.getsize(self.value)[0] - return int(width) - - def value_height(self): - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - height = font.getsize(self.value)[1] - return int(height) - - -class ItemTable(BaseItem): - - obj_type = "table" - - def __init__(self, values, use_alternate_color=False, *args, **kwargs): - - self.values_by_cords = None - self.prepare_values(values) - - super(ItemTable, self).__init__(*args, **kwargs) - self.size_values = None - self.calculate_sizes() - - self.use_alternate_color = use_alternate_color - - def add_item(self, item): - if item.obj_type == "table-item": - return - super(ItemTable, self).add_item(item) - - def fill_data_format(self): - for item in self.values: - item.fill_data_format() - - def prepare_values(self, _values): - values = [] - values_by_cords = [] - row_count = 0 - col_count = 0 - for row in _values: - row_count += 1 - if len(row) > col_count: - col_count = len(row) - - for row_idx in range(row_count): - values_by_cords.append([]) - for col_idx in range(col_count): - values_by_cords[row_idx].append([]) - if col_idx <= len(_values[row_idx]) - 1: - col = _values[row_idx][col_idx] - else: - col = "" - - col_item = TableField(row_idx, col_idx, col, parent=self) - values_by_cords[row_idx][col_idx] = col_item - values.append(col_item) - - self.values = values - self.values_by_cords = values_by_cords - - def calculate_sizes(self): - row_heights = [] - col_widths = [] - for row_idx, row in enumerate(self.values_by_cords): - row_heights.append(0) - for col_idx, col_item in enumerate(row): - if len(col_widths) < col_idx + 1: - col_widths.append(0) - - _width = col_widths[col_idx] - item_width = col_item.width() - if _width < item_width: - col_widths[col_idx] = item_width - - _height = row_heights[row_idx] - item_height = col_item.height() - if _height < item_height: - row_heights[row_idx] = item_height - - self.size_values = (row_heights, col_widths) - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - # TODO border outline styles - drawer.rectangle( - (self.content_pos_start, self.content_pos_end), - fill=bg_color, - outline=None - ) - - for value in self.values: - value.draw(image, drawer) - - def value_width(self): - row_heights, col_widths = self.size_values - width = 0 - for _width in col_widths: - width += _width - - if width != 0: - width -= 1 - return width - - def value_height(self): - row_heights, col_widths = self.size_values - height = 0 - for _height in row_heights: - height += _height - - if height != 0: - height -= 1 - return height - - def content_pos_info_by_cord(self, row_idx, col_idx): - row_heights, col_widths = self.size_values - pos_x = int(self.value_pos_x) - pos_y = int(self.value_pos_y) - width = 0 - height = 0 - for idx, value in enumerate(col_widths): - if col_idx == idx: - width = value - break - pos_x += value - - for idx, value in enumerate(row_heights): - if row_idx == idx: - height = value - break - pos_y += value - - return (pos_x, pos_y, width, height) - - -class TableField(BaseItem): - - obj_type = "table-item" - available_parents = ["table"] - ellide_text = "..." - - def __init__(self, row_idx, col_idx, value, *args, **kwargs): - super(TableField, self).__init__(*args, **kwargs) - self.row_idx = row_idx - self.col_idx = col_idx - self.value = value - - def recalculate_by_width(self, value, max_width): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - padding_right = self.style.get("padding-right") - if padding_right is None: - padding_right = padding - - max_width -= (padding_left + padding_right) - - if not value: - return "" - - word_wrap = self.style.get("word-wrap") - ellide = self.style.get("ellide") - max_lines = self.style.get("max-lines") - - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - val_width = font.getsize(value)[0] - if val_width <= max_width: - return value - - if not ellide and not word_wrap: - # TODO logging - print(( - "Can't draw text because is too long with" - " `word-wrap` and `ellide` turned off" - )) - return "" - - elif ellide and not word_wrap: - max_lines = 1 - - words = [word for word in value.split()] - words_len = len(words) - lines = [] - last_index = None - while True: - start_index = 0 - if last_index is not None: - start_index = int(last_index) + 1 - - line = "" - for idx in range(start_index, words_len): - _word = words[idx] - connector = " " - if line == "": - connector = "" - - _line = connector.join([line, _word]) - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - break - line = _line - last_index = idx - - if line: - lines.append(line) - - if last_index == words_len - 1: - break - - elif last_index is None: - if ellide: - line = "" - for idx, char in enumerate(words[idx]): - _line = line + char + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = line + char - - lines.append(line) - # TODO logging - print("Font size is too big.") - break - - output = "" - if not lines: - return output - - over_max_lines = (max_lines and len(lines) > max_lines) - if not over_max_lines: - return "\n".join([line for line in lines]) - - lines = [lines[idx] for idx in range(max_lines)] - if not ellide: - return "\n".join(lines) - - last_line = lines[-1] - last_line_width = font.getsize(last_line + self.ellide_text)[0] - if last_line_width <= max_width: - lines[-1] += self.ellide_text - return "\n".join([line for line in lines]) - - last_line_words = last_line.split() - if len(last_line_words) == 1: - if max_lines > 1: - # TODO try previous line? - lines[-1] = self.ellide_text - return "\n".join([line for line in lines]) - - line = "" - for idx, word in enumerate(last_line_words): - _line = line + word + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = _line - lines[-1] = line - - return "\n".join([line for line in lines]) - - line = "" - for idx, _word in enumerate(last_line_words): - connector = " " - if line == "": - connector = "" - - _line = connector.join([line, _word + self.ellide_text]) - _line_width = font.getsize(_line)[0] - - if _line_width <= max_width: - line = connector.join([line, _word]) - continue - - if idx != 0: - line += self.ellide_text - break - - if max_lines != 1: - # TODO try previous line? - line = self.ellide_text - break - - for idx, char in enumerate(_word): - _line = line + char + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = line + char - break - - lines[-1] = line - - return "\n".join([line for line in lines]) - - def fill_data_format(self): - value = self.value - if re.match(self.fill_data_regex, value): - value = value.format(**self.fill_data) - - self.orig_value = value - - max_width = self.style.get("max-width") - max_width = self.style.get("width") or max_width - if max_width: - value = self.recalculate_by_width(value, max_width) - - self.value = value - - def content_width(self): - width = self.style.get("width") - if width: - return int(width) - return super(TableField, self).content_width() - - def content_height(self): - return super(TableField, self).content_height() - - def value_width(self): - if not self.value: - return 0 - - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - width = font.getsize_multiline(self.value)[0] + 1 - - min_width = self.style.get("min-height") - if min_width and min_width > width: - width = min_width - - return int(width) - - def value_height(self): - if not self.value: - return 0 - - height = self.style.get("height") - if height: - return int(height) - - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - height = font.getsize_multiline(self.value)[1] + 1 - - min_height = self.style.get("min-height") - if min_height and min_height > height: - height = min_height - - return int(height) - - @property - def item_pos_x(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - return pos_x - - @property - def item_pos_y(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - return pos_y - - @property - def value_pos_x(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - alignment_hor = self.style["alignment-horizontal"].lower() - if alignment_hor in ["center", "centre"]: - pos_x += (width - self.value_width()) / 2 - - elif alignment_hor == "right": - pos_x += width - self.value_width() - - else: - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - pos_x += padding_left - - return int(pos_x) - - @property - def value_pos_y(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - - alignment_ver = self.style["alignment-vertical"].lower() - if alignment_ver in ["center", "centre"]: - pos_y += (height - self.value_height()) / 2 - - elif alignment_ver == "bottom": - pos_y += height - self.value_height() - - else: - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - pos_y += padding_top - - return int(pos_y) - - def draw(self, image, drawer): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - pos_start = (pos_x, pos_y) - pos_end = (pos_x + width, pos_y + height) - bg_color = self.style["bg-color"] - if self.parent.use_alternate_color and (self.row_idx % 2) == 1: - bg_color = self.style["bg-alter-color"] - - if bg_color and bg_color.lower() != "transparent": - # TODO border outline styles - drawer.rectangle( - (pos_start, pos_end), - fill=bg_color, - outline=None - ) - - font_color = self.style["font-color"] - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - - alignment_hor = self.style["alignment-horizontal"].lower() - if alignment_hor == "centre": - alignment_hor = "center" - - drawer.multiline_text( - self.value_pos_start, - self.value, - font=font, - fill=font_color, - align=alignment_hor - ) - - -class FontFactory: - fonts = None - default = None - - @classmethod - def get_font(cls, family, font_size=None, italic=False, bold=False): - if cls.fonts is None: - cls.load_fonts() - - styles = [] - if bold: - styles.append("Bold") - - if italic: - styles.append("Italic") - - if not styles: - styles.append("Regular") - - style = " ".join(styles) - family = family.lower() - family_styles = cls.fonts.get(family) - if not family_styles: - return cls.default - - font = family_styles.get(style) - if font: - if font_size: - font = font.font_variant(size=font_size) - return font - - # Return first found - for font in family_styles: - if font_size: - font = font.font_variant(size=font_size) - return font - - return cls.default - - @classmethod - def load_fonts(cls): - - cls.default = ImageFont.load_default() - - available_font_ext = [".ttf", ".ttc"] - dirs = [] - if sys.platform == "win32": - # check the windows font repository - # NOTE: must use uppercase WINDIR, to work around bugs in - # 1.5.2's os.environ.get() - windir = os.environ.get("WINDIR") - if windir: - dirs.append(os.path.join(windir, "fonts")) - - elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") - if not lindirs: - # According to the freedesktop spec, XDG_DATA_DIRS should - # default to /usr/share - lindirs = "/usr/share" - dirs += [ - os.path.join(lindir, "fonts") for lindir in lindirs.split(":") - ] - - elif sys.platform == "darwin": - dirs += [ - "/Library/Fonts", - "/System/Library/Fonts", - os.path.expanduser("~/Library/Fonts") - ] - - available_fonts = collections.defaultdict(dict) - for directory in dirs: - for walkroot, walkdir, walkfilenames in os.walk(directory): - for walkfilename in walkfilenames: - ext = os.path.splitext(walkfilename)[1] - if ext.lower() not in available_font_ext: - continue - - fontpath = os.path.join(walkroot, walkfilename) - font_obj = ImageFont.truetype(fontpath) - family = font_obj.font.family.lower() - style = font_obj.font.style - available_fonts[family][style] = font_obj - - cls.fonts = available_fonts - -def main(): - cur_folder = os.path.dirname(os.path.abspath(__file__)) - # input_json = os.path.join(cur_folder, "netflix_v01.json") - # input_json = os.path.join(cur_folder, "netflix_v02.json") - input_json = os.path.join(cur_folder, "netflix_v03.json") - with open(input_json) as json_file: - slate_data = json.load(json_file) - - fill_data = { - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", - "project": { - "name": "Project name" - }, - "intent": "WIP", - "version_name": "mei_101_001_0020_slate_NFX_v001", - "date": "2019-08-09", - "shot_type": "2d comp", - "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", - "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "vendor": "DAZZLE", - "shot_name": "SLATE_SIMPLE", - "frame_start": 1001, - "frame_end": 1004, - "duration": 3 - } - width = slate_data["width"] - height = slate_data["height"] - dst_path = slate_data["destination_path"] - style = slate_data.get("style") or {} - - main = MainFrame(width, height, dst_path, fill_data, style=style) - - load_queue = Queue() - for item in slate_data["items"]: - load_queue.put((item, main)) - - while not load_queue.empty(): - item_data, parent = load_queue.get() - - item_type = item_data["type"].lower() - item_style = item_data.get("style", {}) - item_name = item_data.get("name") - - pos_x = None - pos_y = None - if parent.obj_type == "main_frame": - pos_x = item_data.get("pos_x", {}) - pos_y = item_data.get("pos_y", {}) - - kwargs = { - "parent": parent, - "style": item_style, - "name": item_name, - "pos_x": pos_x, - "pos_y": pos_y - } - - if item_type == "layer": - direction = item_data.get("direction", 0) - item_obj = Layer(direction, **kwargs) - for item in item_data.get("items", []): - load_queue.put((item, item_obj)) - - elif item_type == "table": - use_alternate_color = item_data.get("use_alternate_color", False) - values = item_data.get("values") or [] - ItemTable(values, use_alternate_color, **kwargs) - - elif item_type == "image": - path = item_data["path"] - ItemImage(path, **kwargs) - - elif item_type == "rectangle": - ItemRectangle(**kwargs) - - elif item_type == "placeholder": - path = item_data["path"] - ItemPlaceHolder(path, **kwargs) - - else: - # TODO logging - print( - "Slate item not implemented <{}> - skipping".format(item_type) - ) - - main.draw() - print(main.collect_data()) - print("*** Finished") - - -if __name__ == "__main__": - main() diff --git a/pype/scripts/slate/default_style.json b/pype/scripts/slate/default_style.json deleted file mode 100644 index c0f1006a4a..0000000000 --- a/pype/scripts/slate/default_style.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#777777", - "bg-alter-color": "#666666", - "__alignment-vertical__values": ["left", "center", "right"], - "alignment-vertical": "left", - "__alignment-horizontal__values": ["top", "center", "bottom"], - "alignment-horizontal": "top", - "__padding__variants": [ - "padding-top", "padding-right", "padding-bottom", "padding-left" - ], - "padding": 0, - "__margin__variants": [ - "margin-top", "margin-right", "margin-bottom", "margin-left" - ], - "margin": 0, - }, - "layer": { - - }, - "image": { - - }, - "text": { - - }, - "table": { - - }, - "table-item": { - - }, - "table-item-col-0": { - - }, - "#MyName": { - - } -} diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slate/lib.py new file mode 100644 index 0000000000..ca3c0f2e41 --- /dev/null +++ b/pype/scripts/slate/lib.py @@ -0,0 +1,114 @@ +import os +import json +from queue import Queue + +# --- Lines for debug purpose --------------------------------- +import sys +sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") +# ------------------------------------------------------------- + +from slate_base.main_frame import MainFrame +from slate_base.layer import Layer +from slate_base.items import ( + ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder +) + + +def main(fill_data): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + input_json = os.path.join(cur_folder, "netflix_v03.json") + with open(input_json) as json_file: + slate_data = json.load(json_file) + + + width = slate_data["width"] + height = slate_data["height"] + dst_path = slate_data["destination_path"] + style = slate_data.get("style") or {} + + main = MainFrame(width, height, dst_path, fill_data, style=style) + + load_queue = Queue() + for item in slate_data["items"]: + load_queue.put((item, main)) + + while not load_queue.empty(): + item_data, parent = load_queue.get() + + item_type = item_data["type"].lower() + item_style = item_data.get("style", {}) + item_name = item_data.get("name") + + pos_x = item_data.get("pos_x") + pos_y = item_data.get("pos_y") + if parent.obj_type != "main_frame": + if pos_x or pos_y: + # TODO logging + self.log.warning(( + "You have specified `pos_x` and `pos_y` but won't be used." + " Possible only if parent of an item is `main_frame`." + )) + pos_x = None + pos_y = None + + kwargs = { + "parent": parent, + "style": item_style, + "name": item_name, + "pos_x": pos_x, + "pos_y": pos_y + } + + if item_type == "layer": + direction = item_data.get("direction", 0) + item_obj = Layer(direction, **kwargs) + for item in item_data.get("items", []): + load_queue.put((item, item_obj)) + + elif item_type == "table": + use_alternate_color = item_data.get("use_alternate_color", False) + values = item_data.get("values") or [] + ItemTable(values, use_alternate_color, **kwargs) + + elif item_type == "image": + path = item_data["path"] + ItemImage(path, **kwargs) + + elif item_type == "rectangle": + ItemRectangle(**kwargs) + + elif item_type == "placeholder": + path = item_data["path"] + ItemPlaceHolder(path, **kwargs) + + else: + # TODO logging + self.log.warning( + "Slate item not implemented <{}> - skipping".format(item_type) + ) + + main.draw() + print("Slate creation finished") + + +if __name__ == "__main__": + fill_data = { + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "project": { + "name": "Project name" + }, + "intent": "WIP", + "version_name": "mei_101_001_0020_slate_NFX_v001", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", + "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "vendor": "DAZZLE", + "shot_name": "SLATE_SIMPLE", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } + main(fill_data) + # raise NotImplementedError("Slates don't have Implemented args running") diff --git a/pype/scripts/slate/slate.py b/pype/scripts/slate/slate.py deleted file mode 100644 index 4e66f68d21..0000000000 --- a/pype/scripts/slate/slate.py +++ /dev/null @@ -1,355 +0,0 @@ -import textwrap -from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor - - -class TableDraw: - def __init__( - self, image_width, image_height, - rel_pos_x, rel_pos_y, rel_width, - col_fonts=None, col_font_colors=None, - default_font=None, default_font_color=None, - col_alignments=None, rel_col_widths=None, bg_color=None, - alter_bg_color=None, pad=20, - pad_top=None, pad_bottom=None, pad_left=None, pad_right=None - ): - self.image_width = image_width - - pos_x = image_width * rel_pos_x - pos_y = image_height * rel_pos_y - width = image_width * rel_width - - self.pos_x_start = pos_x - self.pos_y_start = pos_y - self.pos_x_end = pos_x + width - self.width = width - - self.rel_col_widths = list(rel_col_widths) - self._col_widths = None - self._col_alignments = col_alignments - - if bg_color and isinstance(bg_color, str): - bg_color = ImageColor.getrgb(bg_color) - - if alter_bg_color and isinstance(alter_bg_color, str): - alter_bg_color = ImageColor.getrgb(alter_bg_color) - - self._bg_color = bg_color - self._alter_bg_color = alter_bg_color - - self.alter_use = False - - if col_fonts: - _col_fonts = [] - for col_font in col_fonts: - font_name, font_size = col_font - font = ImageFont.truetype(font_name, font_size) - _col_fonts.append(font) - col_fonts = _col_fonts - - self._col_fonts = col_fonts - - self._col_font_colors = col_font_colors - - if not default_font: - default_font = ImageFont.truetype("times", 26) - self.default_font = default_font - - if not default_font_color: - default_font_color = "#ffffff" - self.default_font_color = default_font_color - - self.texts = [] - - if pad is None: - pad = 5 - - _pad_top = pad - if pad_top is not None: - _pad_top = pad_top - - _pad_bottom = pad - if pad_bottom is not None: - _pad_bottom = pad_bottom - - _pad_left = pad - if pad_left is not None: - _pad_left = pad_left - - _pad_right = pad - if pad_right is not None: - _pad_right = pad_right - - self.pad_top = _pad_top - self.pad_bottom = _pad_bottom - self.pad_left = _pad_left - self.pad_right = _pad_right - - @property - def col_widths(self): - if self._col_widths is None: - sum_width = 0 - for w in self.rel_col_widths: - sum_width += w - - one_piece = self.width / sum_width - self._col_widths = [] - for w in self.rel_col_widths: - self._col_widths.append(one_piece * w) - - return self._col_widths - - @property - def col_fonts(self): - if self._col_fonts is None: - self._col_fonts = [] - for _ in range(len(self.col_widths)): - self._col_fonts.append(self.default_font) - - elif len(self._col_fonts) < len(self.col_widths): - if isinstance(self._col_fonts, tuple): - self._col_fonts = list(self._col_fonts) - - while len(self._col_fonts) < len(self.col_widths): - self._col_fonts.append(self.default_font) - - return self._col_fonts - - @property - def col_font_colors(self): - if self._col_font_colors is None: - self._col_font_colors = [] - for _ in range(len(self.col_widths)): - self._col_font_colors.append(self.default_font_color) - - elif len(self._col_font_colors) < len(self.col_widths): - if isinstance(self._col_font_colors, tuple): - self._col_font_colors = list(self._col_font_colors) - - while len(self._col_font_colors) < len(self.col_widths): - self._col_font_colors.append(self.default_font_color) - - return self._col_font_colors - - @property - def col_alignments(self): - if self._col_alignments is None: - self._col_alignments = [] - for _ in range(len(self.col_widths)): - self._col_alignments.append("left") - - elif len(self._col_alignments) < len(self.col_widths): - if isinstance(self._col_alignments, tuple): - self._col_alignments = list(self._col_alignments) - - while len(self._col_alignments) < len(self.col_widths): - self._col_alignments.append("left") - - return self._col_alignments - - @property - def bg_color(self): - if self.alter_use is True: - value = self.alter_bg_color - self.alter_use = False - else: - value = self._bg_color - self.alter_use = True - return value - - @property - def alter_bg_color(self): - if self._alter_bg_color: - return self._alter_bg_color - return self.bg_color - - def add_texts(self, texts): - if isinstance(texts, str): - texts = [texts] - - for text in texts: - if isinstance(text, str): - text = [text] - - if len(text) > len(self.rel_col_widths): - for _ in (len(text) - len(self.rel_col_widths)): - self.rel_col_widths.append(1) - for _t in self.texts: - _t.append("") - - self.texts.append(text) - - def draw(self, drawer): - y_pos = self.pos_y_start - for texts in self.texts: - max_height = None - cols_data = [] - for _idx, col in enumerate(texts): - width = self.col_widths[_idx] - font = self.col_fonts[_idx] - lines, line_height = self.lines_height_by_width( - drawer, col, width - self.pad_left - self.pad_right, font - ) - row_height = line_height * len(lines) - if max_height is None or row_height > max_height: - max_height = row_height - - cols_data.append({ - "lines": lines, - "line_height": line_height - }) - - drawer.rectangle( - ( - (self.pos_x_start, y_pos), - ( - self.pos_x_end, - y_pos + max_height + self.pad_top + self.pad_bottom - ) - ), - fill=self.bg_color - ) - - pos_x_start = self.pos_x_start + self.pad_left - for col, col_data in enumerate(cols_data): - lines = col_data["lines"] - line_height = col_data["line_height"] - alignment = self.col_alignments[col] - x_offset = self.col_widths[col] - font = self.col_fonts[col] - font_color = self.col_font_colors[col] - for idx, line_data in enumerate(lines): - line = line_data["text"] - line_width = line_data["width"] - if alignment == "left": - x_start = pos_x_start + self.pad_left - elif alignment == "right": - x_start = ( - pos_x_start + x_offset - line_width - - self.pad_right - self.pad_left - ) - else: - # TODO else - x_start = pos_x_start + self.pad_left - - drawer.text( - ( - x_start, - y_pos + (idx * line_height) + self.pad_top - ), - line, - font=font, - fill=font_color - ) - pos_x_start += x_offset - - y_pos += max_height + self.pad_top + self.pad_bottom - - def lines_height_by_width(self, drawer, text, width, font): - lines = [] - lines.append([part for part in text.split() if part]) - - line = 0 - while True: - thistext = lines[line] - line = line + 1 - if not thistext: - break - newline = [] - - while True: - _textwidth = drawer.textsize(" ".join(thistext), font)[0] - if ( - _textwidth <= width - ): - break - elif _textwidth > width and len(thistext) == 1: - # TODO raise error? - break - - val = thistext.pop(-1) - - if not val: - break - newline.insert(0, val) - - if len(newline) > 0: - lines.append(newline) - else: - break - - _lines = [] - height = None - for line_items in lines: - line = " ".join(line_items) - (width, _height) = drawer.textsize(line, font) - if height is None or height < _height: - height = _height - - _lines.append({ - "width": width, - "text": line - }) - - return (_lines, height) - - -width = 1920 -height = 1080 -# width = 800 -# height = 600 -bg_color_hex = "#242424" -bg_color = ImageColor.getrgb(bg_color_hex) - -base = Image.new('RGB', (width, height), color=bg_color) - -texts = [ - ("Version name:", "mei_101_001_0020_slate_NFX_v001"), - ("Date:", "2019-08-09"), - ("Shot Types:", "2d comp"), - # ("Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.") -] -text_widths_rel = (2, 8) -col_alignments = ("right", "left") -fonts = (("arial", 20), ("times", 26)) -font_colors = ("#999999", "#ffffff") - -table_color_hex = "#212121" -table_alter_color_hex = "#272727" - -drawer = ImageDraw.Draw(base) -table_d = TableDraw( - width, height, - 0.1, 0.1, 0.5, - col_fonts=fonts, - col_font_colors=font_colors, - rel_col_widths=text_widths_rel, - col_alignments=col_alignments, - bg_color=table_color_hex, - alter_bg_color=table_alter_color_hex, - pad_top=20, pad_bottom=20, pad_left=5, pad_right=5 -) - -table_d.add_texts(texts) -table_d.draw(drawer) - -image_path = r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\image.jpg" -image = Image.open(image_path) -img_width, img_height = image.size - -rel_image_width = 0.3 -rel_image_pos_x = 0.65 -rel_image_pos_y = 0.1 -image_pos_x = int(width * rel_image_pos_x) -image_pos_y = int(width * rel_image_pos_y) - -new_width = int(width * rel_image_width) -new_height = int(new_width * img_height / img_width) -image = image.resize((new_width, new_height), Image.ANTIALIAS) - -# mask = Image.new("L", image.size, 255) - -base.paste(image, (image_pos_x, image_pos_y)) -base.save(r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\test{}x{}.jpg".format( - width, height -)) -base.show() diff --git a/pype/scripts/slate/slate_base/__init__.py b/pype/scripts/slate/slate_base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/scripts/slate/slate_base/base.py b/pype/scripts/slate/slate_base/base.py new file mode 100644 index 0000000000..35a3b6af6d --- /dev/null +++ b/pype/scripts/slate/slate_base/base.py @@ -0,0 +1,373 @@ +import os +import re +import logging +import copy +import json +from uuid import uuid4 + + +def load_default_style(): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + default_json_path = os.path.join(cur_folder, "default_style.json") + with open(default_json_path, "r") as _file: + data = _file.read() + return json.loads(data) + + +class BaseObj: + """Base Object for slates.""" + + obj_type = None + available_parents = [] + all_style_keys = [ + "font-family", "font-size", "font-color", "font-bold", "font-italic", + "bg-color", "bg-alter-color", + "alignment-horizontal", "alignment-vertical", + "padding", "padding-left", "padding-right", + "padding-top", "padding-bottom", + "margin", "margin-left", "margin-right", + "margin-top", "margin-bottom", "width", "height", + "fill", "word-wrap", "ellide", "max-lines" + ] + fill_data_regex = r"{[^}]+}" + + def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): + if not self.obj_type: + raise NotImplementedError( + "Class don't have set object type <{}>".format( + self.__class__.__name__ + ) + ) + + parent_obj_type = None + if parent: + parent_obj_type = parent.obj_type + + if parent_obj_type not in self.available_parents: + expected_parents = ", ".join(self.available_parents) + raise Exception(( + "Invalid parent <{}> for <{}>. Expected <{}>" + ).format( + parent.__class__.__name__, self.obj_type, expected_parents + )) + + self.parent = parent + self._style = style + + self.id = uuid4() + self.name = name + self.items = {} + + self._pos_x = pos_x or 0 + self._pos_y = pos_y or 0 + + log_parts = [] + module = self.__class__.__module__ + if module and module != "__main__": + log_parts.append(module) + log_parts.append(self.__class__.__name__) + self.log = logging.getLogger(".".join(log_parts)) + + if parent: + parent.add_item(self) + + def fill_data_format(self): + return + + @property + def fill_data(self): + return self.parent.fill_data + + @property + def main_style(self): + return load_default_style() + + def height(self): + raise NotImplementedError( + "Attribute `height` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + def width(self): + raise NotImplementedError( + "Attribute `width` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + def collect_data(self): + return None + + def find_item(self, obj_type=None, name=None): + obj_type_fits = False + name_fits = False + if obj_type is None or self.obj_type == obj_type: + obj_type_fits = True + + if name is None or self.name != name: + name_fits = True + + output = [] + if obj_type_fits and name_fits: + output.append(self) + + if not self.items: + return output + + for item in self.items.values(): + output.extend( + item.find_item(obj_type=obj_type, name=name) + ) + return output + + @property + def full_style(self): + if self.parent is not None: + style = dict(val for val in self.parent.full_style.items()) + else: + style = self.main_style + + for key, value in self._style.items(): + if key in self.all_style_keys: + # TODO which variant is right? + style[self.obj_type][key] = value + # style["*"][key] = value + else: + if key not in style: + style[key] = {} + + if isinstance(style[key], dict): + style[key].update(value) + else: + style[key] = value + + return style + + def get_style_for_obj_type(self, obj_type, style=None): + if not style: + style = copy.deepcopy(self.full_style) + + base = style.get("*") or {} + obj_specific = style.get(obj_type) or {} + name_specific = {} + if self.name: + name = str(self.name) + if not name.startswith("#"): + name = "#" + name + name_specific = style.get(name) or {} + + if obj_type == "table-item": + col_regex = r"table-item-col\[([\d\-, ]+)*\]" + row_regex = r"table-item-row\[([\d\-, ]+)*\]" + field_regex = ( + r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" + ) + # STRICT field regex (not allowed spaces) + # fild_regex = r"table-item-field\[(\d+:\d+)*\]" + + def get_indexes_from_regex_match(result, field=False): + group = result.group(1) + indexes = [] + if field: + return [ + int(part.strip()) for part in group.strip().split(":") + ] + + parts = group.strip().split(",") + for part in parts: + part = part.strip() + if "-" not in part: + indexes.append(int(part)) + continue + + sub_parts = [ + int(sub.strip()) for sub in part.split("-") + ] + if len(sub_parts) != 2: + # TODO logging + self.log.warning("Invalid range '{}'".format(part)) + continue + + for idx in range(sub_parts[0], sub_parts[1]+1): + indexes.append(idx) + return indexes + + for key, value in style.items(): + if not key.startswith(obj_type): + continue + + result = re.search(col_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.col_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(row_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.row_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(field_regex, key) + if result: + col_idx, row_idx = get_indexes_from_regex_match( + result, True + ) + if self.col_idx == col_idx and self.row_idx == row_idx: + obj_specific.update(value) + + output = {} + output.update(base) + output.update(obj_specific) + output.update(name_specific) + + return output + + @property + def style(self): + return self.get_style_for_obj_type(self.obj_type) + + @property + def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return int(self._pos_x) + return 0 + + @property + def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return int(self._pos_y) + return 0 + + @property + def content_pos_x(self): + pos_x = self.item_pos_x + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + pos_x += margin_left + + return pos_x + + @property + def content_pos_y(self): + pos_y = self.item_pos_y + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + return pos_y + margin_top + + @property + def value_pos_x(self): + pos_x = int(self.content_pos_x) + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + pos_x += padding_left + + return pos_x + + @property + def value_pos_y(self): + pos_y = int(self.content_pos_y) + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + pos_y += padding_top + + return pos_y + + @property + def value_pos_start(self): + return (self.value_pos_x, self.value_pos_y) + + @property + def value_pos_end(self): + pos_x, pos_y = self.value_pos_start + pos_x += self.width() + pos_y += self.height() + return (pos_x, pos_y) + + @property + def content_pos_start(self): + return (self.content_pos_x, self.content_pos_y) + + @property + def content_pos_end(self): + pos_x, pos_y = self.content_pos_start + pos_x += self.content_width() + pos_y += self.content_height() + return (pos_x, pos_y) + + def value_width(self): + raise NotImplementedError( + "Attribute is not implemented <{}>".format( + self.__class__.__name__ + ) + ) + + def value_height(self): + raise NotImplementedError( + "Attribute is not implemented for <{}>".format( + self.__class__.__name__ + ) + ) + + def content_width(self): + width = self.value_width() + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + + return width + padding_left + padding_right + + def content_height(self): + height = self.value_height() + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + padding_bottom = self.style.get("padding-bottom") + if padding_bottom is None: + padding_bottom = padding + + return height + padding_top + padding_bottom + + def width(self): + width = self.content_width() + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return width + margin_left + margin_right + + def height(self): + height = self.content_height() + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return height + margin_bottom + margin_top + + def add_item(self, item): + self.items[item.id] = item + item.fill_data_format() + + + def reset(self): + for item in self.items.values(): + item.reset() diff --git a/pype/scripts/slate/slate_base/default_style.json b/pype/scripts/slate/slate_base/default_style.json new file mode 100644 index 0000000000..d0748846a5 --- /dev/null +++ b/pype/scripts/slate/slate_base/default_style.json @@ -0,0 +1,58 @@ +{ + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": true, + "ellide": true, + "max-lines": null + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "fill": true + }, + "image": { + "padding": 0, + "margin": 0, + "fill": true + }, + "placeholder": { + "padding": 0, + "margin": 0, + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 0, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + } +} diff --git a/pype/scripts/slate/slate_base/font_factory.py b/pype/scripts/slate/slate_base/font_factory.py new file mode 100644 index 0000000000..77df9a40a7 --- /dev/null +++ b/pype/scripts/slate/slate_base/font_factory.py @@ -0,0 +1,93 @@ +import os +import sys +import collections + +from PIL import ImageFont + + +class FontFactory: + fonts = None + default = None + + @classmethod + def get_font(cls, family, font_size=None, italic=False, bold=False): + if cls.fonts is None: + cls.load_fonts() + + styles = [] + if bold: + styles.append("Bold") + + if italic: + styles.append("Italic") + + if not styles: + styles.append("Regular") + + style = " ".join(styles) + family = family.lower() + family_styles = cls.fonts.get(family) + if not family_styles: + return cls.default + + font = family_styles.get(style) + if font: + if font_size: + font = font.font_variant(size=font_size) + return font + + # Return first found + for font in family_styles: + if font_size: + font = font.font_variant(size=font_size) + return font + + return cls.default + + @classmethod + def load_fonts(cls): + + cls.default = ImageFont.load_default() + + available_font_ext = [".ttf", ".ttc"] + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + + elif sys.platform in ("linux", "linux2"): + lindirs = os.environ.get("XDG_DATA_DIRS", "") + if not lindirs: + # According to the freedesktop spec, XDG_DATA_DIRS should + # default to /usr/share + lindirs = "/usr/share" + dirs += [ + os.path.join(lindir, "fonts") for lindir in lindirs.split(":") + ] + + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts") + ] + + available_fonts = collections.defaultdict(dict) + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + ext = os.path.splitext(walkfilename)[1] + if ext.lower() not in available_font_ext: + continue + + fontpath = os.path.join(walkroot, walkfilename) + font_obj = ImageFont.truetype(fontpath) + family = font_obj.font.family.lower() + style = font_obj.font.style + available_fonts[family][style] = font_obj + + cls.fonts = available_fonts diff --git a/pype/scripts/slate/slate_base/items.py b/pype/scripts/slate/slate_base/items.py new file mode 100644 index 0000000000..ea31443f80 --- /dev/null +++ b/pype/scripts/slate/slate_base/items.py @@ -0,0 +1,666 @@ +import re +from PIL import Image + +from .base import BaseObj +from .font_factory import FontFactory + + +class BaseItem(BaseObj): + available_parents = ["main_frame", "layer"] + + @property + def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return self._pos_x + return self.parent.child_pos_x(self.id) + + @property + def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_y + return self.parent.child_pos_y(self.id) + + def add_item(self, *args, **kwargs): + raise Exception("Can't add item to an item, use layers instead.") + + def draw(self, image, drawer): + raise NotImplementedError( + "Method `draw` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + +class ItemImage(BaseItem): + obj_type = "image" + + def __init__(self, image_path, *args, **kwargs): + super(ItemImage, self).__init__(*args, **kwargs) + self.image_path = image_path + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + + def draw(self, image, drawer): + source_image = Image.open(os.path.normpath(self.image_path)) + paste_image = source_image.resize( + (self.value_width(), self.value_height()), + Image.ANTIALIAS + ) + image.paste( + paste_image, + (self.value_pos_x, self.value_pos_y) + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + +class ItemRectangle(BaseItem): + obj_type = "rectangle" + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + fill = self.style.get("fill", False) + kwargs = {} + if fill: + kwargs["fill"] = bg_color + else: + kwargs["outline"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + +class ItemPlaceHolder(BaseItem): + obj_type = "placeholder" + + def __init__(self, image_path, *args, **kwargs): + self.image_path = image_path + super(ItemPlaceHolder, self).__init__(*args, **kwargs) + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + + kwargs = {} + if bg_color != "tranparent": + kwargs["fill"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + def collect_data(self): + return { + "pos_x": self.value_pos_x, + "pos_y": self.value_pos_y, + "width": self.value_width(), + "height": self.value_height(), + "path": self.image_path + } + + +class ItemText(BaseItem): + obj_type = "text" + + def __init__(self, value, *args, **kwargs): + super(ItemText, self).__init__(*args, **kwargs) + self.value = value + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + # TODO border outline styles + drawer.rectangle( + (self.content_pos_start, self.content_pos_end), + fill=bg_color, + outline=None + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + drawer.text( + self.value_pos_start, + self.value, + font=font, + fill=font_color + ) + + def value_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize(self.value)[0] + return int(width) + + def value_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize(self.value)[1] + return int(height) + + +class ItemTable(BaseItem): + + obj_type = "table" + + def __init__(self, values, use_alternate_color=False, *args, **kwargs): + + self.values_by_cords = None + self.prepare_values(values) + + super(ItemTable, self).__init__(*args, **kwargs) + self.size_values = None + self.calculate_sizes() + + self.use_alternate_color = use_alternate_color + + def add_item(self, item): + if item.obj_type == "table-item": + return + super(ItemTable, self).add_item(item) + + def fill_data_format(self): + for item in self.values: + item.fill_data_format() + + def prepare_values(self, _values): + values = [] + values_by_cords = [] + row_count = 0 + col_count = 0 + for row in _values: + row_count += 1 + if len(row) > col_count: + col_count = len(row) + + for row_idx in range(row_count): + values_by_cords.append([]) + for col_idx in range(col_count): + values_by_cords[row_idx].append([]) + if col_idx <= len(_values[row_idx]) - 1: + col = _values[row_idx][col_idx] + else: + col = "" + + col_item = TableField(row_idx, col_idx, col, parent=self) + values_by_cords[row_idx][col_idx] = col_item + values.append(col_item) + + self.values = values + self.values_by_cords = values_by_cords + + def calculate_sizes(self): + row_heights = [] + col_widths = [] + for row_idx, row in enumerate(self.values_by_cords): + row_heights.append(0) + for col_idx, col_item in enumerate(row): + if len(col_widths) < col_idx + 1: + col_widths.append(0) + + _width = col_widths[col_idx] + item_width = col_item.width() + if _width < item_width: + col_widths[col_idx] = item_width + + _height = row_heights[row_idx] + item_height = col_item.height() + if _height < item_height: + row_heights[row_idx] = item_height + + self.size_values = (row_heights, col_widths) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + # TODO border outline styles + drawer.rectangle( + (self.content_pos_start, self.content_pos_end), + fill=bg_color, + outline=None + ) + + for value in self.values: + value.draw(image, drawer) + + def value_width(self): + row_heights, col_widths = self.size_values + width = 0 + for _width in col_widths: + width += _width + + if width != 0: + width -= 1 + return width + + def value_height(self): + row_heights, col_widths = self.size_values + height = 0 + for _height in row_heights: + height += _height + + if height != 0: + height -= 1 + return height + + def content_pos_info_by_cord(self, row_idx, col_idx): + row_heights, col_widths = self.size_values + pos_x = int(self.value_pos_x) + pos_y = int(self.value_pos_y) + width = 0 + height = 0 + for idx, value in enumerate(col_widths): + if col_idx == idx: + width = value + break + pos_x += value + + for idx, value in enumerate(row_heights): + if row_idx == idx: + height = value + break + pos_y += value + + return (pos_x, pos_y, width, height) + + +class TableField(BaseItem): + + obj_type = "table-item" + available_parents = ["table"] + ellide_text = "..." + + def __init__(self, row_idx, col_idx, value, *args, **kwargs): + super(TableField, self).__init__(*args, **kwargs) + self.row_idx = row_idx + self.col_idx = col_idx + self.value = value + + def recalculate_by_width(self, value, max_width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + + max_width -= (padding_left + padding_right) + + if not value: + return "" + + word_wrap = self.style.get("word-wrap") + ellide = self.style.get("ellide") + max_lines = self.style.get("max-lines") + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + val_width = font.getsize(value)[0] + if val_width <= max_width: + return value + + if not ellide and not word_wrap: + # TODO logging + self.log.warning(( + "Can't draw text because is too long with" + " `word-wrap` and `ellide` turned off <{}>" + ).format(value)) + return "" + + elif ellide and not word_wrap: + max_lines = 1 + + words = [word for word in value.split()] + words_len = len(words) + lines = [] + last_index = None + while True: + start_index = 0 + if last_index is not None: + start_index = int(last_index) + 1 + + line = "" + for idx in range(start_index, words_len): + _word = words[idx] + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word]) + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + break + line = _line + last_index = idx + + if line: + lines.append(line) + + if last_index == words_len - 1: + break + + elif last_index is None: + add_message = "" + if ellide: + add_message = " String was shortened to `{}`." + line = "" + for idx, char in enumerate(words[idx]): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = line + char + + lines.append(line) + # TODO logging + self.log.warning(( + "Font size is too big.{} <{}>" + ).format(add_message, value)) + break + + output = "" + if not lines: + return output + + over_max_lines = (max_lines and len(lines) > max_lines) + if not over_max_lines: + return "\n".join([line for line in lines]) + + lines = [lines[idx] for idx in range(max_lines)] + if not ellide: + return "\n".join(lines) + + last_line = lines[-1] + last_line_width = font.getsize(last_line + self.ellide_text)[0] + if last_line_width <= max_width: + lines[-1] += self.ellide_text + return "\n".join([line for line in lines]) + + last_line_words = last_line.split() + if len(last_line_words) == 1: + if max_lines > 1: + # TODO try previous line? + lines[-1] = self.ellide_text + return "\n".join([line for line in lines]) + + line = "" + for idx, word in enumerate(last_line_words): + _line = line + word + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + lines[-1] = line + + return "\n".join([line for line in lines]) + + line = "" + for idx, _word in enumerate(last_line_words): + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word + self.ellide_text]) + _line_width = font.getsize(_line)[0] + + if _line_width <= max_width: + line = connector.join([line, _word]) + continue + + if idx != 0: + line += self.ellide_text + break + + if max_lines != 1: + # TODO try previous line? + line = self.ellide_text + break + + for idx, char in enumerate(_word): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = line + char + break + + lines[-1] = line + + return "\n".join([line for line in lines]) + + def fill_data_format(self): + value = self.value + if re.match(self.fill_data_regex, value): + value = value.format(**self.fill_data) + + self.orig_value = value + + max_width = self.style.get("max-width") + max_width = self.style.get("width") or max_width + if max_width: + value = self.recalculate_by_width(value, max_width) + + self.value = value + + def content_width(self): + width = self.style.get("width") + if width: + return int(width) + return super(TableField, self).content_width() + + def content_height(self): + return super(TableField, self).content_height() + + def value_width(self): + if not self.value: + return 0 + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize_multiline(self.value)[0] + 1 + + min_width = self.style.get("min-height") + if min_width and min_width > width: + width = min_width + + return int(width) + + def value_height(self): + if not self.value: + return 0 + + height = self.style.get("height") + if height: + return int(height) + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize_multiline(self.value)[1] + 1 + + min_height = self.style.get("min-height") + if min_height and min_height > height: + height = min_height + + return int(height) + + @property + def item_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + return pos_x + + @property + def item_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + return pos_y + + @property + def value_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor in ["center", "centre"]: + pos_x += (width - self.value_width()) / 2 + + elif alignment_hor == "right": + pos_x += width - self.value_width() + + else: + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + pos_x += padding_left + + return int(pos_x) + + @property + def value_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + + alignment_ver = self.style["alignment-vertical"].lower() + if alignment_ver in ["center", "centre"]: + pos_y += (height - self.value_height()) / 2 + + elif alignment_ver == "bottom": + pos_y += height - self.value_height() + + else: + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + pos_y += padding_top + + return int(pos_y) + + def draw(self, image, drawer): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + pos_start = (pos_x, pos_y) + pos_end = (pos_x + width, pos_y + height) + bg_color = self.style["bg-color"] + if self.parent.use_alternate_color and (self.row_idx % 2) == 1: + bg_color = self.style["bg-alter-color"] + + if bg_color and bg_color.lower() != "transparent": + # TODO border outline styles + drawer.rectangle( + (pos_start, pos_end), + fill=bg_color, + outline=None + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor == "centre": + alignment_hor = "center" + + drawer.multiline_text( + self.value_pos_start, + self.value, + font=font, + fill=font_color, + align=alignment_hor + ) diff --git a/pype/scripts/slate/slate_base/layer.py b/pype/scripts/slate/slate_base/layer.py new file mode 100644 index 0000000000..ea3a3de53e --- /dev/null +++ b/pype/scripts/slate/slate_base/layer.py @@ -0,0 +1,139 @@ +from .base import BaseObj + + +class Layer(BaseObj): + obj_type = "layer" + available_parents = ["main_frame", "layer"] + + # Direction can be 0=vertical/ 1=horizontal + def __init__(self, direction=0, *args, **kwargs): + super(Layer, self).__init__(*args, **kwargs) + self._direction = direction + + @property + def item_pos_x(self): + if self.parent.obj_type == self.obj_type: + pos_x = self.parent.child_pos_x(self.id) + elif self.parent.obj_type == "main_frame": + pos_x = self._pos_x + else: + pos_x = self.parent.value_pos_x + return int(pos_x) + + @property + def item_pos_y(self): + if self.parent.obj_type == self.obj_type: + pos_y = self.parent.child_pos_y(self.id) + elif self.parent.obj_type == "main_frame": + pos_y = self._pos_y + else: + pos_y = self.parent.value_pos_y + + return int(pos_y) + + @property + def direction(self): + if self._direction not in (0, 1): + self.log.warning(( + "Direction of Layer must be 0 or 1 " + "(0 is horizontal / 1 is vertical)! Setting to 0." + )) + return 0 + return self._direction + + def child_pos_x(self, item_id): + pos_x = self.value_pos_x + alignment_hor = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break + + if self.direction == 1: + for id, _item in self.items.items(): + if item_id == id: + break + + pos_x += _item.width() + if _item.obj_type not in ["image", "placeholder"]: + pos_x += 1 + + else: + if alignment_hor in ["center", "centre"]: + pos_x += (self.content_width() - item.content_width()) / 2 + + elif alignment_hor == "right": + pos_x += self.content_width() - item.content_width() + + else: + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + pos_x += margin_left + + return int(pos_x) + + def child_pos_y(self, item_id): + pos_y = self.value_pos_y + alignment_ver = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break + + if self.direction != 1: + for id, item in self.items.items(): + if item_id == id: + break + pos_y += item.height() + if item.obj_type not in ["image", "placeholder"]: + pos_y += 1 + + else: + if alignment_ver in ["center", "centre"]: + pos_y += (self.content_height() - item.content_height()) / 2 + + elif alignment_ver == "bottom": + pos_y += self.content_height() - item.content_height() + + return int(pos_y) + + def value_height(self): + height = 0 + for item in self.items.values(): + if self.direction == 1: + if height > item.height(): + continue + # times 1 because won't get object pointer but number + height = item.height() + else: + height += item.height() + + # TODO this is not right + min_height = self.style.get("min-height") + if min_height and min_height > height: + return min_height + return height + + def value_width(self): + width = 0 + for item in self.items.values(): + if self.direction == 0: + if width > item.width(): + continue + # times 1 because won't get object pointer but number + width = item.width() + else: + width += item.width() + + min_width = self.style.get("min-width") + if min_width and min_width > width: + return min_width + return width + + def draw(self, image, drawer): + for item in self.items.values(): + item.draw(image, drawer) diff --git a/pype/scripts/slate/slate_base/main_frame.py b/pype/scripts/slate/slate_base/main_frame.py new file mode 100644 index 0000000000..837e752aae --- /dev/null +++ b/pype/scripts/slate/slate_base/main_frame.py @@ -0,0 +1,77 @@ +import os +import re +from PIL import Image, ImageDraw + +from .base import BaseObj + + +class MainFrame(BaseObj): + + obj_type = "main_frame" + available_parents = [None] + + def __init__( + self, width, height, destination_path, fill_data={}, *args, **kwargs + ): + kwargs["parent"] = None + super(MainFrame, self).__init__(*args, **kwargs) + self._width = width + self._height = height + self.dst_path = destination_path + self._fill_data = fill_data + self.fill_data_format() + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.dst_path): + self.dst_path = self.dst_path.format(**self.fill_data) + + @property + def fill_data(self): + return self._fill_data + + def value_width(self): + width = 0 + for item in self.items.values(): + width += item.width() + return width + + def value_height(self): + height = 0 + for item in self.items.values(): + height += item.height() + return height + + def width(self): + return self._width + + def height(self): + return self._height + + def draw(self, path=None): + dir_path = os.path.dirname(self.dst_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + bg_color = self.style["bg-color"] + image = Image.new("RGB", (self.width(), self.height()), color=bg_color) + drawer = ImageDraw.Draw(image) + for item in self.items.values(): + item.draw(image, drawer) + + image.save(self.dst_path) + self.reset() + + def collect_data(self): + output = {} + output["width"] = self.width() + output["height"] = self.height() + output["slate_path"] = self.dst_path + + placeholders = self.find_item(obj_type="placeholder") + placeholders_data = [] + for placeholder in placeholders: + placeholders_data.append(placeholder.collect_data()) + + output["placeholders"] = placeholders_data + + return output diff --git a/pype/scripts/slate/style_schema.json b/pype/scripts/slate/style_schema.json deleted file mode 100644 index dab6151df5..0000000000 --- a/pype/scripts/slate/style_schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "alignment": { - "description": "Alignment of item", - "type": "string", - "enum": ["left", "right", "center"], - "example": "left" - }, - "font-family": { - "description": "Font type", - "type": "string", - "example": "Arial" - }, - "font-size": { - "description": "Font size", - "type": "integer", - "example": 26 - }, - "font-color": { - "description": "Font color", - "type": "string", - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#ffffff" - }, - "font-bold": { - "description": "Font boldness", - "type": "boolean", - "example": True - }, - "bg-color": { - "description": "Background color, None means transparent", - "type": ["string", None], - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#333" - }, - "bg-alter-color": None, -} From f54a3e981a12ff078fa70b0e6f8fa7f5e55a79a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:07:59 +0100 Subject: [PATCH 033/327] created basic slate usage --- pype/scripts/slate/lib.py | 87 ++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slate/lib.py index ca3c0f2e41..750046269e 100644 --- a/pype/scripts/slate/lib.py +++ b/pype/scripts/slate/lib.py @@ -1,26 +1,46 @@ import os import json +import logging from queue import Queue -# --- Lines for debug purpose --------------------------------- -import sys -sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") -# ------------------------------------------------------------- - -from slate_base.main_frame import MainFrame -from slate_base.layer import Layer -from slate_base.items import ( +from .slate_base.main_frame import MainFrame +from .slate_base.layer import Layer +from .slate_base.items import ( ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder ) +from pypeapp import config -def main(fill_data): - cur_folder = os.path.dirname(os.path.abspath(__file__)) - input_json = os.path.join(cur_folder, "netflix_v03.json") - with open(input_json) as json_file: - slate_data = json.load(json_file) +log = logging.getLogger(__name__) +RequiredSlateKeys = ["width", "height", "destination_path"] + + +def create_slates(fill_data, slate_name): + presets = config.get_presets() + slate_presets = ( + presets + .get("tools", {}) + .get("slates") + ) or {} + slate_data = slate_presets.get(slate_name) + + if not slate_data: + log.error("Slate data of <{}> does not exists.") + return False + + missing_keys = [] + for key in RequiredSlateKeys: + if key not in slate_data: + missing_keys.append("`{}`".format(key)) + + if missing_keys: + log.error("Slate data of <{}> miss required keys: {}".format( + slate_name, ", ".join(missing_keys) + )) + return False + width = slate_data["width"] height = slate_data["height"] dst_path = slate_data["destination_path"] @@ -44,7 +64,7 @@ def main(fill_data): if parent.obj_type != "main_frame": if pos_x or pos_y: # TODO logging - self.log.warning(( + log.warning(( "You have specified `pos_x` and `pos_y` but won't be used." " Possible only if parent of an item is `main_frame`." )) @@ -83,32 +103,43 @@ def main(fill_data): else: # TODO logging - self.log.warning( - "Slate item not implemented <{}> - skipping".format(item_type) + log.warning( + "Not implemented object type `{}` - skipping".format(item_type) ) main.draw() - print("Slate creation finished") + log.debug("Slate creation finished") -if __name__ == "__main__": +def example(): fill_data = { - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "destination_path": "PATH/TO/OUTPUT/FILE", "project": { - "name": "Project name" + "name": "Testing project" }, "intent": "WIP", - "version_name": "mei_101_001_0020_slate_NFX_v001", + "version_name": "seq01_sh0100_compositing_v01", "date": "2019-08-09", "shot_type": "2d comp", - "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", - "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "vendor": "DAZZLE", - "shot_name": "SLATE_SIMPLE", + "submission_note": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." + " Aenean commodo ligula eget dolor. Aenean massa." + " Cum sociis natoque penatibus et magnis dis parturient montes," + " nascetur ridiculus mus. Donec quam felis, ultricies nec," + " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" + " enim. Donec pede justo, fringilla vel," + " aliquet nec, vulputate eget, arcu." + ), + "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", + "color_bar_path": "PATH/TO/COLOR/BAR/FILE", + "vendor": "Our Studio", + "shot_name": "sh0100", "frame_start": 1001, "frame_end": 1004, "duration": 3 } - main(fill_data) - # raise NotImplementedError("Slates don't have Implemented args running") + create_slates(fill_data, "example_HD") + + +if __name__ == "__main__": + raise NotImplementedError("Slates don't have Implemented args running") From 5b0e8fe8d0dac8c8e8a70e3557cc53a866932c7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:08:17 +0100 Subject: [PATCH 034/327] remove netflix examples --- pype/scripts/slate/netflix_v01.json | 201 -------------------------- pype/scripts/slate/netflix_v02.json | 213 ---------------------------- pype/scripts/slate/netflix_v03.json | 213 ---------------------------- 3 files changed, 627 deletions(-) delete mode 100644 pype/scripts/slate/netflix_v01.json delete mode 100644 pype/scripts/slate/netflix_v02.json delete mode 100644 pype/scripts/slate/netflix_v03.json diff --git a/pype/scripts/slate/netflix_v01.json b/pype/scripts/slate/netflix_v01.json deleted file mode 100644 index 9a5974839d..0000000000 --- a/pype/scripts/slate/netflix_v01.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v01.png", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top", - "padding": 0, - "margin": 0 - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "bg-color": "#252525" - }, - "table": { - "bg-color": "transparent" - }, - "table-item": { - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "padding": 5, - "padding-bottom": 10, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "pos_x": 27, - "pos_y": 27, - "style": { - "width": 1094, - "height": 1000 - }, - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent" - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "SAMPLE"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "mei_101_001_0020_slate_NFX_v001"], - ["Date:", "2019-08-09"], - ["Shot Types:", "2d comp"], - ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 2 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "pos_x": 1164, - "pos_y": 26, - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "DAZZLE"], - ["Shot Name:", "SLATE_SIMPLE"], - ["Frames:", "0 - 1 (2)"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] -} diff --git a/pype/scripts/slate/netflix_v02.json b/pype/scripts/slate/netflix_v02.json deleted file mode 100644 index f373ed8134..0000000000 --- a/pype/scripts/slate/netflix_v02.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v02.png", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "direction": 1, - "name": "MainLayer", - "style": { - "#MainLayer": { - "width": 1094, - "height": 1000, - "margin": 25, - "padding": 0 - }, - "#LeftSide": { - "margin-right": 25 - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent", - "padding-bottom": 20 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "SAMPLE"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "mei_101_001_0020_slate_NFX_v001"], - ["Date:", "2019-08-09"], - ["Shot Types:", "2d comp"], - ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 2 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "DAZZLE"], - ["Shot Name:", "SLATE_SIMPLE"], - ["Frames:", "0 - 1 (2)"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] - }] -} diff --git a/pype/scripts/slate/netflix_v03.json b/pype/scripts/slate/netflix_v03.json deleted file mode 100644 index 975cb60133..0000000000 --- a/pype/scripts/slate/netflix_v03.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "{destination_path}", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "direction": 1, - "name": "MainLayer", - "style": { - "#MainLayer": { - "width": 1094, - "height": 1000, - "margin": 25, - "padding": 0 - }, - "#LeftSide": { - "margin-right": 25 - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent", - "padding-bottom": 20 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "{project[name]}"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "{intent}"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "{version_name}"], - ["Date:", "{date}"], - ["Shot Types:", "{shot_type}"], - ["Submission Note:", "{submission_note}"] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 4 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "{thumbnail_path}", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "{color_bar_path}", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "{vendor}"], - ["Shot Name:", "{shot_name}"], - ["Frames:", "{frame_start} - {frame_end} ({duration})"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] - }] -} From b3f4673248a8ec06dbc44d6457dbb095688aaa20 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:09:16 +0100 Subject: [PATCH 035/327] renamed slate folder to slates folder --- pype/scripts/{slate => slates}/lib.py | 0 pype/scripts/{slate => slates}/slate_base/__init__.py | 0 pype/scripts/{slate => slates}/slate_base/base.py | 0 pype/scripts/{slate => slates}/slate_base/default_style.json | 0 pype/scripts/{slate => slates}/slate_base/font_factory.py | 0 pype/scripts/{slate => slates}/slate_base/items.py | 0 pype/scripts/{slate => slates}/slate_base/layer.py | 0 pype/scripts/{slate => slates}/slate_base/main_frame.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename pype/scripts/{slate => slates}/lib.py (100%) rename pype/scripts/{slate => slates}/slate_base/__init__.py (100%) rename pype/scripts/{slate => slates}/slate_base/base.py (100%) rename pype/scripts/{slate => slates}/slate_base/default_style.json (100%) rename pype/scripts/{slate => slates}/slate_base/font_factory.py (100%) rename pype/scripts/{slate => slates}/slate_base/items.py (100%) rename pype/scripts/{slate => slates}/slate_base/layer.py (100%) rename pype/scripts/{slate => slates}/slate_base/main_frame.py (100%) diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slates/lib.py similarity index 100% rename from pype/scripts/slate/lib.py rename to pype/scripts/slates/lib.py diff --git a/pype/scripts/slate/slate_base/__init__.py b/pype/scripts/slates/slate_base/__init__.py similarity index 100% rename from pype/scripts/slate/slate_base/__init__.py rename to pype/scripts/slates/slate_base/__init__.py diff --git a/pype/scripts/slate/slate_base/base.py b/pype/scripts/slates/slate_base/base.py similarity index 100% rename from pype/scripts/slate/slate_base/base.py rename to pype/scripts/slates/slate_base/base.py diff --git a/pype/scripts/slate/slate_base/default_style.json b/pype/scripts/slates/slate_base/default_style.json similarity index 100% rename from pype/scripts/slate/slate_base/default_style.json rename to pype/scripts/slates/slate_base/default_style.json diff --git a/pype/scripts/slate/slate_base/font_factory.py b/pype/scripts/slates/slate_base/font_factory.py similarity index 100% rename from pype/scripts/slate/slate_base/font_factory.py rename to pype/scripts/slates/slate_base/font_factory.py diff --git a/pype/scripts/slate/slate_base/items.py b/pype/scripts/slates/slate_base/items.py similarity index 100% rename from pype/scripts/slate/slate_base/items.py rename to pype/scripts/slates/slate_base/items.py diff --git a/pype/scripts/slate/slate_base/layer.py b/pype/scripts/slates/slate_base/layer.py similarity index 100% rename from pype/scripts/slate/slate_base/layer.py rename to pype/scripts/slates/slate_base/layer.py diff --git a/pype/scripts/slate/slate_base/main_frame.py b/pype/scripts/slates/slate_base/main_frame.py similarity index 100% rename from pype/scripts/slate/slate_base/main_frame.py rename to pype/scripts/slates/slate_base/main_frame.py From 16bc4dcc6098dd1e514aac06851a83630a7fdfef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:16:02 +0100 Subject: [PATCH 036/327] exchange col_idx and row_idx for field style --- pype/scripts/slates/slate_base/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/slates/slate_base/base.py b/pype/scripts/slates/slate_base/base.py index 35a3b6af6d..35ef46769c 100644 --- a/pype/scripts/slates/slate_base/base.py +++ b/pype/scripts/slates/slate_base/base.py @@ -213,7 +213,7 @@ class BaseObj: result = re.search(field_regex, key) if result: - col_idx, row_idx = get_indexes_from_regex_match( + row_idx, col_idx = get_indexes_from_regex_match( result, True ) if self.col_idx == col_idx and self.row_idx == row_idx: From f72a19457bafe003ebc1c7a5deb12fdfbeb414bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jan 2020 10:28:54 +0100 Subject: [PATCH 037/327] added example for testing --- pype/scripts/slates/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/lib.py index 750046269e..a73f87e82f 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/lib.py @@ -1,5 +1,3 @@ -import os -import json import logging from queue import Queue @@ -112,6 +110,10 @@ def create_slates(fill_data, slate_name): def example(): + # import sys + # sys.append(r"PATH/TO/PILLOW/PACKAGE") + # sys.append(r"PATH/TO/PYPE-SETUP") + fill_data = { "destination_path": "PATH/TO/OUTPUT/FILE", "project": { @@ -142,4 +144,4 @@ def example(): if __name__ == "__main__": - raise NotImplementedError("Slates don't have Implemented args running") + example() From 631942327edc0469834c5cbb2e328a4caae0586e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 10:43:51 +0000 Subject: [PATCH 038/327] Creation and publishing of a rig in Blender --- pype/blender/plugin.py | 7 +++ pype/plugins/blender/create/create_rig.py | 32 +++++++++++++ pype/plugins/blender/publish/collect_rig.py | 53 +++++++++++++++++++++ pype/plugins/blender/publish/extract_rig.py | 47 ++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 pype/plugins/blender/create/create_rig.py create mode 100644 pype/plugins/blender/publish/collect_rig.py create mode 100644 pype/plugins/blender/publish/extract_rig.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index ad5a259785..eaa429c989 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -17,6 +17,13 @@ def model_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: name = f"{namespace}:{name}" return name +def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: + """Return a consistent name for a rig asset.""" + name = f"{asset}_{subset}" + if namespace: + name = f"{namespace}:{name}" + return name + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py new file mode 100644 index 0000000000..01eb524eef --- /dev/null +++ b/pype/plugins/blender/create/create_rig.py @@ -0,0 +1,32 @@ +"""Create a rig asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateRig(Creator): + """Artist-friendly rig with controls to direct motion""" + + name = "rigMain" + label = "Rig" + family = "rig" + icon = "cube" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.rig_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py new file mode 100644 index 0000000000..a4b30541f6 --- /dev/null +++ b/pype/plugins/blender/publish/collect_rig.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectRig(pyblish.api.ContextPlugin): + """Collect the data of a rig.""" + + hosts = ["blender"] + label = "Collect Rig" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_rig_collections() -> Generator: + """Return all 'rig' collections. + + Check if the family is 'rig' and if it doesn't have the + representation set. If the representation is set, it is a loaded rig + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'rig' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the rigs from the current Blender scene.""" + collections = self.get_rig_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/extract_rig.py b/pype/plugins/blender/publish/extract_rig.py new file mode 100644 index 0000000000..8a3c83d07c --- /dev/null +++ b/pype/plugins/blender/publish/extract_rig.py @@ -0,0 +1,47 @@ +import os +import avalon.blender.workio + +import pype.api + + +class ExtractRig(pype.api.Extractor): + """Extract as rig.""" + + label = "Rig" + hosts = ["blender"] + families = ["rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From fd6bdc9aa5bb48a3ca252e493281d26f45120470 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 10:45:50 +0000 Subject: [PATCH 039/327] Loading and removal of rigs from Blender scenes --- pype/blender/plugin.py | 20 ++ pype/plugins/blender/load/load_rig.py | 337 ++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 pype/plugins/blender/load/load_rig.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index eaa429c989..c85e6df990 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -24,6 +24,26 @@ def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: name = f"{namespace}:{name}" return name +def create_blender_context( obj: Optional[bpy.types.Object] = None ): + """Create a new Blender context. If an object is passed as + parameter, it is set as selected and active. + """ + for win in bpy.context.window_manager.windows: + for area in win.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + override_context = { + 'window': win, + 'screen': win.screen, + 'area': area, + 'region': region, + 'scene': bpy.context.scene, + 'active_object': obj, + 'selected_objects': [obj] + } + return override_context + raise Exception( "Could not create a custom Blender context." ) class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py new file mode 100644 index 0000000000..f3c9e49f53 --- /dev/null +++ b/pype/plugins/blender/load/load_rig.py @@ -0,0 +1,337 @@ +"""Load a rig asset in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + +class BlendRigLoader(pype.blender.AssetLoader): + """Load rigs from a .blend file. + + Because they come from a .blend file we can simply link the collection that + contains the model. There is no further need to 'containerise' it. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["rig"] + representations = ["blend"] + + label = "Link Rig" + icon = "code-fork" + color = "orange" + + @staticmethod + def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: + """Find the collection(s) with name, loaded from libpath. + + Note: + It is assumed that only 1 matching collection is found. + """ + for collection in bpy.data.collections: + if collection.name != name: + continue + if collection.library is None: + continue + if not collection.library.filepath: + continue + collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) + normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) + if collection_lib_path == normalized_libpath: + return collection + return None + + @staticmethod + def _collection_contains_object( + collection: bpy.types.Collection, object: bpy.types.Object + ) -> bool: + """Check if the collection contains the object.""" + for obj in collection.objects: + if obj == object: + return True + return False + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.rig_name(asset, subset) + container_name = pype.blender.plugin.rig_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + container = bpy.data.collections[lib_container] + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + override_context = pype.blender.plugin.create_blender_context() + bpy.ops.object.collection_instance_add( override_context, name = container_name + "_CON" ) + + override_context = pype.blender.plugin.create_blender_context( bpy.data.objects[container_name + "_CON"] ) + bpy.ops.object.make_override_library( override_context ) + bpy.ops.object.delete( override_context ) + + container_metadata = container.get( 'avalon' ) + + object_names_list = [] + + for c in bpy.data.collections: + + if c.name == container_name + "_CON" and c.library is None: + + for obj in c.objects: + + scene.collection.objects.link( obj ) + c.objects.unlink( obj ) + + if not obj.get("avalon"): + obj["avalon"] = dict() + + avalon_info = obj["avalon"] + avalon_info.update( { "container_name": container_name } ) + + object_names_list.append( obj.name ) + + bpy.data.collections.remove( c ) + + container_metadata["objects"] = object_names_list + + bpy.ops.object.select_all( action = 'DESELECT' ) + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def load(self, + context: dict, + name: Optional[str] = None, + namespace: Optional[str] = None, + options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: + """Load asset via database + + Arguments: + context: Full parenthood of representation to load + name: Use pre-defined name + namespace: Use pre-defined namespace + options: Additional settings dictionary + """ + # TODO (jasper): make it possible to add the asset several times by + # just re-using the collection + assert Path(self.fname).exists(), f"{self.fname} doesn't exist." + + self.process_asset( + context=context, + name=name, + namespace=namespace, + options=options, + ) + + # Only containerise if anything was loaded by the Loader. + nodes = self[:] + if not nodes: + return None + + # Only containerise if it's not already a collection from a .blend file. + representation = context["representation"]["name"] + if representation != "blend": + from avalon.blender.pipeline import containerise + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__, + ) + + asset = context["asset"]["name"] + subset = context["subset"]["name"] + instance_name = pype.blender.plugin.rig_name(asset, subset, namespace) + + return self._get_instance_collection(instance_name, nodes) + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + collection_libpath = ( + self._get_library_from_container(collection).filepath + ) + print( collection_libpath ) + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + print( normalized_collection_libpath ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + print( normalized_libpath ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + # Let Blender's garbage collection take care of removing the library + # itself after removing the objects. + objects_to_remove = set() + collection_objects = list() + collection_objects[:] = collection.objects + for obj in collection_objects: + # Unlink every object + collection.objects.unlink(obj) + remove_obj = True + for coll in [ + coll for coll in bpy.data.collections + if coll != collection + ]: + if ( + coll.objects and + self._collection_contains_object(coll, obj) + ): + remove_obj = False + if remove_obj: + objects_to_remove.add(obj) + + for obj in objects_to_remove: + # Only delete objects that are not used elsewhere + bpy.data.objects.remove(obj) + + instance_empties = [ + obj for obj in collection.users_dupli_group + if obj.name in collection.name + ] + if instance_empties: + instance_empty = instance_empties[0] + container_name = instance_empty["avalon"]["container_name"] + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [container_name] + + new_collection = self._get_lib_collection(container_name, libpath) + if new_collection is None: + raise ValueError( + "A matching collection '{container_name}' " + "should have been found in: {libpath}" + ) + + for obj in new_collection.objects: + collection.objects.link(obj) + bpy.data.collections.remove(new_collection) + # Update the representation on the collection + avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_prop["representation"] = str(representation["_id"]) + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + print( container["objectName"] ) + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + instance_objects = list(collection.objects) + + data = collection.get( "avalon" ) + object_names = data["objects"] + + for obj in instance_objects: + bpy.data.objects.remove(obj) + + for name in object_names: + bpy.data.objects.remove( bpy.data.objects[name] ) + + bpy.data.collections.remove(collection) + + return True From e88fc5cb4891f0de74822551258687d5e29226ce Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 11:49:38 +0000 Subject: [PATCH 040/327] We now use references to objects in the metadata instead of names --- pype/plugins/blender/load/load_rig.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index f3c9e49f53..70cf6e781a 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -107,7 +107,7 @@ class BlendRigLoader(pype.blender.AssetLoader): container_metadata = container.get( 'avalon' ) - object_names_list = [] + objects_list = [] for c in bpy.data.collections: @@ -124,11 +124,11 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info = obj["avalon"] avalon_info.update( { "container_name": container_name } ) - object_names_list.append( obj.name ) + objects_list.append( obj ) bpy.data.collections.remove( c ) - container_metadata["objects"] = object_names_list + container_metadata["objects"] = objects_list bpy.ops.object.select_all( action = 'DESELECT' ) @@ -324,13 +324,13 @@ class BlendRigLoader(pype.blender.AssetLoader): instance_objects = list(collection.objects) data = collection.get( "avalon" ) - object_names = data["objects"] + objects = data["objects"] for obj in instance_objects: bpy.data.objects.remove(obj) - for name in object_names: - bpy.data.objects.remove( bpy.data.objects[name] ) + for obj in objects: + bpy.data.objects.remove( obj ) bpy.data.collections.remove(collection) From c123ed757ced7a0fc7bd9b82ce4824e03561573d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 15:31:44 +0000 Subject: [PATCH 041/327] Improved handling of rigs using avalon container for metadata only --- pype/plugins/blender/load/load_rig.py | 46 +++++++++++---------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 70cf6e781a..b348f99728 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -81,12 +81,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene + bpy.data.collections.new( lib_container ) container = bpy.data.collections[lib_container] container.name = container_name @@ -98,35 +93,34 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - override_context = pype.blender.plugin.create_blender_context() - bpy.ops.object.collection_instance_add( override_context, name = container_name + "_CON" ) - - override_context = pype.blender.plugin.create_blender_context( bpy.data.objects[container_name + "_CON"] ) - bpy.ops.object.make_override_library( override_context ) - bpy.ops.object.delete( override_context ) - container_metadata = container.get( 'avalon' ) objects_list = [] - for c in bpy.data.collections: + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (data_from, data_to): - if c.name == container_name + "_CON" and c.library is None: + data_to.collections = [lib_container] - for obj in c.objects: + scene = bpy.context.scene - scene.collection.objects.link( obj ) - c.objects.unlink( obj ) + models = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] - if not obj.get("avalon"): - obj["avalon"] = dict() + for obj in models + armatures: - avalon_info = obj["avalon"] - avalon_info.update( { "container_name": container_name } ) + scene.collection.objects.link( obj ) - objects_list.append( obj ) + obj = obj.make_local() - bpy.data.collections.remove( c ) + if not obj.get("avalon"): + + obj["avalon"] = dict() + + avalon_info = obj["avalon"] + avalon_info.update( { "container_name": container_name } ) + objects_list.append( obj ) container_metadata["objects"] = objects_list @@ -321,14 +315,10 @@ class BlendRigLoader(pype.blender.AssetLoader): assert not (collection.children), ( "Nested collections are not supported." ) - instance_objects = list(collection.objects) data = collection.get( "avalon" ) objects = data["objects"] - for obj in instance_objects: - bpy.data.objects.remove(obj) - for obj in objects: bpy.data.objects.remove( obj ) From 88b9ceccab25d1785ab73849d83b8ba7c21e8f98 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 15:45:17 +0000 Subject: [PATCH 042/327] More comments for clarity --- pype/plugins/blender/load/load_rig.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index b348f99728..75aa515c66 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -105,10 +105,13 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - models = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] - for obj in models + armatures: + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: scene.collection.objects.link( obj ) @@ -122,6 +125,7 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info.update( { "container_name": container_name } ) objects_list.append( obj ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list bpy.ops.object.select_all( action = 'DESELECT' ) From fc1779387149902705034f9d2101e7cf5a1acf72 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 5 Feb 2020 15:20:15 +0000 Subject: [PATCH 043/327] Implemented update for rigs --- pype/plugins/blender/load/load_rig.py | 124 +++++++++++++------------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 75aa515c66..294366c41b 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -93,14 +93,14 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( 'avalon' ) + container_metadata = container.get( avalon.blender.pipeline.AVALON_PROPERTY ) - objects_list = [] + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (data_from, data_to): - data_to.collections = [lib_container] scene = bpy.context.scene @@ -108,6 +108,8 @@ class BlendRigLoader(pype.blender.AssetLoader): meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + objects_list = [] + # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. @@ -117,17 +119,19 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() - if not obj.get("avalon"): + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - obj["avalon"] = dict() + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj["avalon"] + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] avalon_info.update( { "container_name": container_name } ) objects_list.append( obj ) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list + bpy.data.collections.remove( bpy.data.collections[lib_container] ) + bpy.ops.object.select_all( action = 'DESELECT' ) nodes = list(container.objects) @@ -198,6 +202,7 @@ class BlendRigLoader(pype.blender.AssetLoader): collection = bpy.data.collections.get( container["objectName"] ) + libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() @@ -222,18 +227,14 @@ class BlendRigLoader(pype.blender.AssetLoader): assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) - collection_libpath = ( - self._get_library_from_container(collection).filepath - ) - print( collection_libpath ) + + collection_libpath = container["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) - print( normalized_collection_libpath ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - print( normalized_libpath ) logger.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, @@ -242,58 +243,63 @@ class BlendRigLoader(pype.blender.AssetLoader): if normalized_collection_libpath == normalized_libpath: logger.info("Library already loaded, not updating...") return - # Let Blender's garbage collection take care of removing the library - # itself after removing the objects. - objects_to_remove = set() - collection_objects = list() - collection_objects[:] = collection.objects - for obj in collection_objects: - # Unlink every object - collection.objects.unlink(obj) - remove_obj = True - for coll in [ - coll for coll in bpy.data.collections - if coll != collection - ]: - if ( - coll.objects and - self._collection_contains_object(coll, obj) - ): - remove_obj = False - if remove_obj: - objects_to_remove.add(obj) - for obj in objects_to_remove: - # Only delete objects that are not used elsewhere - bpy.data.objects.remove(obj) + # Get the armature of the rig + armatures = [ obj for obj in container["objects"] if obj.type == 'ARMATURE' ] + assert( len( armatures ) == 1 ) - instance_empties = [ - obj for obj in collection.users_dupli_group - if obj.name in collection.name - ] - if instance_empties: - instance_empty = instance_empties[0] - container_name = instance_empty["avalon"]["container_name"] + action = armatures[0].animation_data.action + + for obj in container["objects"]: + bpy.data.objects.remove( obj ) + + lib_container = container["lib_container"] relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative ) as (_, data_to): - data_to.collections = [container_name] + data_to.collections = [lib_container] - new_collection = self._get_lib_collection(container_name, libpath) - if new_collection is None: - raise ValueError( - "A matching collection '{container_name}' " - "should have been found in: {libpath}" - ) + scene = bpy.context.scene - for obj in new_collection.objects: - collection.objects.link(obj) - bpy.data.collections.remove(new_collection) - # Update the representation on the collection - avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_prop["representation"] = str(representation["_id"]) + meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + objects_list = [] + + assert( len( armatures ) == 1 ) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + scene.collection.objects.link( obj ) + + obj = obj.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update( { "container_name": collection.name } ) + objects_list.append( obj ) + + if obj.type == 'ARMATURE' and action is not None: + + obj.animation_data.action = action + + collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.data.collections.remove( bpy.data.collections[lib_container] ) + + bpy.ops.object.select_all( action = 'DESELECT' ) def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -309,8 +315,6 @@ class BlendRigLoader(pype.blender.AssetLoader): No nested collections are supported at the moment! """ - print( container["objectName"] ) - collection = bpy.data.collections.get( container["objectName"] ) @@ -320,12 +324,12 @@ class BlendRigLoader(pype.blender.AssetLoader): "Nested collections are not supported." ) - data = collection.get( "avalon" ) - objects = data["objects"] + collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY ) + objects = collection_metadata["objects"] for obj in objects: bpy.data.objects.remove( obj ) - bpy.data.collections.remove(collection) + bpy.data.collections.remove( collection ) return True From 4bd4c6e811f52a4cfc74551d53b3a9fde2e857a3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 5 Feb 2020 16:49:27 +0000 Subject: [PATCH 044/327] The data in the objects is made local as well. Not having this would cause problems with the keyframing of shape keys and custom data. --- pype/plugins/blender/load/load_rig.py | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 294366c41b..fa0e1c52b2 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -119,6 +119,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() + obj.data.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() @@ -228,7 +230,9 @@ class BlendRigLoader(pype.blender.AssetLoader): f"Unsupported file: {libpath}" ) - collection_libpath = container["libpath"] + collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -245,15 +249,19 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [ obj for obj in container["objects"] if obj.type == 'ARMATURE' ] + armatures = [ obj for obj in collection_metadata["objects"] if obj.type == 'ARMATURE' ] assert( len( armatures ) == 1 ) action = armatures[0].animation_data.action - for obj in container["objects"]: - bpy.data.objects.remove( obj ) + for obj in collection_metadata["objects"]: - lib_container = container["lib_container"] + if obj.type == 'ARMATURE': + bpy.data.armatures.remove( obj.data ) + elif obj.type == 'MESH': + bpy.data.meshes.remove( obj.data ) + + lib_container = collection_metadata["lib_container"] relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( @@ -278,6 +286,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() + obj.data.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() @@ -290,8 +300,6 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action - collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) - # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) @@ -328,7 +336,11 @@ class BlendRigLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] for obj in objects: - bpy.data.objects.remove( obj ) + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove( obj.data ) + elif obj.type == 'MESH': + bpy.data.meshes.remove( obj.data ) bpy.data.collections.remove( collection ) From 6f6482a58b4aa328af07ccaae9c2e66ccf7ecc7f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 7 Feb 2020 11:39:14 +0000 Subject: [PATCH 045/327] Rig is linked in a collection, instead of the generic scene collection --- pype/plugins/blender/load/load_rig.py | 79 ++++++++++++++++----------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index fa0e1c52b2..7ea131a54c 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -12,6 +12,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + class BlendRigLoader(pype.blender.AssetLoader): """Load rigs from a .blend file. @@ -44,8 +45,10 @@ class BlendRigLoader(pype.blender.AssetLoader): continue if not collection.library.filepath: continue - collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) + collection_lib_path = str( + Path(bpy.path.abspath(collection.library.filepath)).resolve()) + normalized_libpath = str( + Path(bpy.path.abspath(str(libpath))).resolve()) if collection_lib_path == normalized_libpath: return collection return None @@ -81,7 +84,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.data.collections.new( lib_container ) + bpy.data.collections.new(lib_container) container = bpy.data.collections[lib_container] container.name = container_name @@ -93,7 +96,8 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( avalon.blender.pipeline.AVALON_PROPERTY ) + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -105,8 +109,13 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] - armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] @@ -115,8 +124,6 @@ class BlendRigLoader(pype.blender.AssetLoader): # when it is made local. for obj in meshes + armatures: - scene.collection.objects.link( obj ) - obj = obj.make_local() obj.data.make_local() @@ -126,15 +133,13 @@ class BlendRigLoader(pype.blender.AssetLoader): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update( { "container_name": container_name } ) - objects_list.append( obj ) + avalon_info.update({"container_name": container_name}) + objects_list.append(obj) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.data.collections.remove( bpy.data.collections[lib_container] ) - - bpy.ops.object.select_all( action = 'DESELECT' ) + bpy.ops.object.select_all(action='DESELECT') nodes = list(container.objects) nodes.append(container) @@ -230,7 +235,8 @@ class BlendRigLoader(pype.blender.AssetLoader): f"Unsupported file: {libpath}" ) - collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -249,20 +255,23 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [ obj for obj in collection_metadata["objects"] if obj.type == 'ARMATURE' ] - assert( len( armatures ) == 1 ) + armatures = [obj for obj in collection_metadata["objects"] + if obj.type == 'ARMATURE'] + assert(len(armatures) == 1) action = armatures[0].animation_data.action for obj in collection_metadata["objects"]: if obj.type == 'ARMATURE': - bpy.data.armatures.remove( obj.data ) + bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': - bpy.data.meshes.remove( obj.data ) + bpy.data.meshes.remove(obj.data) lib_container = collection_metadata["lib_container"] + bpy.data.collections.remove(bpy.data.collections[lib_container]) + relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative @@ -271,19 +280,22 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] - armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] - assert( len( armatures ) == 1 ) + assert(len(armatures) == 1) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - scene.collection.objects.link( obj ) - obj = obj.make_local() obj.data.make_local() @@ -293,8 +305,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update( { "container_name": collection.name } ) - objects_list.append( obj ) + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) if obj.type == 'ARMATURE' and action is not None: @@ -305,9 +317,7 @@ class BlendRigLoader(pype.blender.AssetLoader): collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) - bpy.data.collections.remove( bpy.data.collections[lib_container] ) - - bpy.ops.object.select_all( action = 'DESELECT' ) + bpy.ops.object.select_all(action='DESELECT') def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -332,16 +342,19 @@ class BlendRigLoader(pype.blender.AssetLoader): "Nested collections are not supported." ) - collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY ) + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] for obj in objects: if obj.type == 'ARMATURE': - bpy.data.armatures.remove( obj.data ) + bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': - bpy.data.meshes.remove( obj.data ) - - bpy.data.collections.remove( collection ) + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) return True From 65104882db7a44ba3d5e213aae9f7cee0c572c93 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 7 Feb 2020 15:09:21 +0000 Subject: [PATCH 046/327] Changed handling of models for consistency --- pype/plugins/blender/load/load_model.py | 196 ++++++++++++------------ pype/plugins/blender/load/load_rig.py | 39 +---- 2 files changed, 98 insertions(+), 137 deletions(-) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index bd6db17650..bb9f2250be 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -12,7 +12,6 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") - class BlendModelLoader(pype.blender.AssetLoader): """Load models from a .blend file. @@ -31,36 +30,6 @@ class BlendModelLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod - def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: - """Find the collection(s) with name, loaded from libpath. - - Note: - It is assumed that only 1 matching collection is found. - """ - for collection in bpy.data.collections: - if collection.name != name: - continue - if collection.library is None: - continue - if not collection.library.filepath: - continue - collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) - if collection_lib_path == normalized_libpath: - return collection - return None - - @staticmethod - def _collection_contains_object( - collection: bpy.types.Collection, object: bpy.types.Object - ) -> bool: - """Check if the collection contains the object.""" - for obj in collection.objects: - if obj == object: - return True - return False - def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -82,25 +51,8 @@ class BlendModelLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - instance_empty = bpy.data.objects.new( - container_name, None - ) - if not instance_empty.get("avalon"): - instance_empty["avalon"] = dict() - avalon_info = instance_empty["avalon"] - avalon_info.update({"container_name": container_name}) - scene.collection.objects.link(instance_empty) - instance_empty.instance_type = 'COLLECTION' - container = bpy.data.collections[lib_container] + container = bpy.data.collections.new(lib_container) container.name = container_name - instance_empty.instance_collection = container - container.make_local() avalon.blender.pipeline.containerise_existing( container, name, @@ -109,9 +61,47 @@ class BlendModelLoader(pype.blender.AssetLoader): self.__class__.__name__, ) + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + for obj in rig_container.objects: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + nodes = list(container.objects) nodes.append(container) - nodes.append(instance_empty) self[:] = nodes return nodes @@ -154,9 +144,11 @@ class BlendModelLoader(pype.blender.AssetLoader): assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) - collection_libpath = ( - self._get_library_from_container(collection).filepath - ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -171,58 +163,52 @@ class BlendModelLoader(pype.blender.AssetLoader): if normalized_collection_libpath == normalized_libpath: logger.info("Library already loaded, not updating...") return - # Let Blender's garbage collection take care of removing the library - # itself after removing the objects. - objects_to_remove = set() - collection_objects = list() - collection_objects[:] = collection.objects - for obj in collection_objects: - # Unlink every object - collection.objects.unlink(obj) - remove_obj = True - for coll in [ - coll for coll in bpy.data.collections - if coll != collection - ]: - if ( - coll.objects and - self._collection_contains_object(coll, obj) - ): - remove_obj = False - if remove_obj: - objects_to_remove.add(obj) - for obj in objects_to_remove: - # Only delete objects that are not used elsewhere - bpy.data.objects.remove(obj) + for obj in collection_metadata["objects"]: - instance_empties = [ - obj for obj in collection.users_dupli_group - if obj.name in collection.name - ] - if instance_empties: - instance_empty = instance_empties[0] - container_name = instance_empty["avalon"]["container_name"] + bpy.data.meshes.remove(obj.data) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative ) as (_, data_to): - data_to.collections = [container_name] + data_to.collections = [lib_container] - new_collection = self._get_lib_collection(container_name, libpath) - if new_collection is None: - raise ValueError( - "A matching collection '{container_name}' " - "should have been found in: {libpath}" - ) + scene = bpy.context.scene - for obj in new_collection.objects: - collection.objects.link(obj) - bpy.data.collections.remove(new_collection) - # Update the representation on the collection - avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_prop["representation"] = str(representation["_id"]) + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in rig_container.objects: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -245,10 +231,17 @@ class BlendModelLoader(pype.blender.AssetLoader): assert not (collection.children), ( "Nested collections are not supported." ) - instance_parents = list(collection.users_dupli_group) - instance_objects = list(collection.objects) - for obj in instance_objects + instance_parents: - bpy.data.objects.remove(obj) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) bpy.data.collections.remove(collection) return True @@ -281,7 +274,8 @@ class CacheModelLoader(pype.blender.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - raise NotImplementedError("Loading of Alembic files is not yet implemented.") + raise NotImplementedError( + "Loading of Alembic files is not yet implemented.") # TODO (jasper): implement Alembic import. libpath = self.fname diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 7ea131a54c..8593440624 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -31,38 +31,6 @@ class BlendRigLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod - def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: - """Find the collection(s) with name, loaded from libpath. - - Note: - It is assumed that only 1 matching collection is found. - """ - for collection in bpy.data.collections: - if collection.name != name: - continue - if collection.library is None: - continue - if not collection.library.filepath: - continue - collection_lib_path = str( - Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str( - Path(bpy.path.abspath(str(libpath))).resolve()) - if collection_lib_path == normalized_libpath: - return collection - return None - - @staticmethod - def _collection_contains_object( - collection: bpy.types.Collection, object: bpy.types.Object - ) -> bool: - """Check if the collection contains the object.""" - for obj in collection.objects: - if obj == object: - return True - return False - def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -84,9 +52,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.data.collections.new(lib_container) - - container = bpy.data.collections[lib_container] + container = bpy.data.collections.new(lib_container) container.name = container_name avalon.blender.pipeline.containerise_existing( container, @@ -104,7 +70,7 @@ class BlendRigLoader(pype.blender.AssetLoader): with bpy.data.libraries.load( libpath, link=True, relative=relative - ) as (data_from, data_to): + ) as (_, data_to): data_to.collections = [lib_container] scene = bpy.context.scene @@ -134,6 +100,7 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) + objects_list.append(obj) # Save the list of objects in the metadata container From 951dcfca3e7f9c02e8d878fea4d693f950b7fada Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 10 Feb 2020 14:45:50 +0000 Subject: [PATCH 047/327] Code optimization --- pype/blender/plugin.py | 13 ++---- pype/plugins/blender/create/create_model.py | 2 +- pype/plugins/blender/create/create_rig.py | 4 +- pype/plugins/blender/load/load_model.py | 15 +++--- pype/plugins/blender/load/load_rig.py | 51 +-------------------- 5 files changed, 16 insertions(+), 69 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index c85e6df990..b441714c0d 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -10,15 +10,8 @@ from avalon import api VALID_EXTENSIONS = [".blend"] -def model_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: - """Return a consistent name for a model asset.""" - name = f"{asset}_{subset}" - if namespace: - name = f"{namespace}:{name}" - return name - -def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: - """Return a consistent name for a rig asset.""" +def asset_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: + """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: name = f"{namespace}:{name}" @@ -149,7 +142,7 @@ class AssetLoader(api.Loader): asset = context["asset"]["name"] subset = context["subset"]["name"] - instance_name = model_name(asset, subset, namespace) + instance_name = asset_name(asset, subset, namespace) return self._get_instance_collection(instance_name, nodes) diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 7301073f05..a3b2ffc55b 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -19,7 +19,7 @@ class CreateModel(Creator): asset = self.data["asset"] subset = self.data["subset"] - name = pype.blender.plugin.model_name(asset, subset) + name = pype.blender.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) self.data['task'] = api.Session.get('AVALON_TASK') diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 01eb524eef..5d83fafdd3 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -12,14 +12,14 @@ class CreateRig(Creator): name = "rigMain" label = "Rig" family = "rig" - icon = "cube" + icon = "wheelchair" def process(self): import pype.blender asset = self.data["asset"] subset = self.data["subset"] - name = pype.blender.plugin.rig_name(asset, subset) + name = pype.blender.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) self.data['task'] = api.Session.get('AVALON_TASK') diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index bb9f2250be..cde4109a7c 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -12,6 +12,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + class BlendModelLoader(pype.blender.AssetLoader): """Load models from a .blend file. @@ -45,8 +46,8 @@ class BlendModelLoader(pype.blender.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.blender.plugin.model_name(asset, subset) - container_name = pype.blender.plugin.model_name( + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) relative = bpy.context.preferences.filepaths.use_relative_paths @@ -76,11 +77,11 @@ class BlendModelLoader(pype.blender.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + model_container = scene.collection.children[lib_container].make_local() objects_list = [] - for obj in rig_container.objects: + for obj in model_container.objects: obj = obj.make_local() @@ -182,14 +183,14 @@ class BlendModelLoader(pype.blender.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + model_container = scene.collection.children[lib_container].make_local() objects_list = [] # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in rig_container.objects: + for obj in model_container.objects: obj = obj.make_local() @@ -283,7 +284,7 @@ class CacheModelLoader(pype.blender.AssetLoader): subset = context["subset"]["name"] # TODO (jasper): evaluate use of namespace which is 'alien' to Blender. lib_container = container_name = ( - pype.blender.plugin.model_name(asset, subset, namespace) + pype.blender.plugin.asset_name(asset, subset, namespace) ) relative = bpy.context.preferences.filepaths.use_relative_paths diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 8593440624..361850c51b 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -46,8 +46,8 @@ class BlendRigLoader(pype.blender.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.blender.plugin.rig_name(asset, subset) - container_name = pype.blender.plugin.rig_name( + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) relative = bpy.context.preferences.filepaths.use_relative_paths @@ -113,53 +113,6 @@ class BlendRigLoader(pype.blender.AssetLoader): self[:] = nodes return nodes - def load(self, - context: dict, - name: Optional[str] = None, - namespace: Optional[str] = None, - options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: - """Load asset via database - - Arguments: - context: Full parenthood of representation to load - name: Use pre-defined name - namespace: Use pre-defined namespace - options: Additional settings dictionary - """ - # TODO (jasper): make it possible to add the asset several times by - # just re-using the collection - assert Path(self.fname).exists(), f"{self.fname} doesn't exist." - - self.process_asset( - context=context, - name=name, - namespace=namespace, - options=options, - ) - - # Only containerise if anything was loaded by the Loader. - nodes = self[:] - if not nodes: - return None - - # Only containerise if it's not already a collection from a .blend file. - representation = context["representation"]["name"] - if representation != "blend": - from avalon.blender.pipeline import containerise - return containerise( - name=name, - namespace=namespace, - nodes=nodes, - context=context, - loader=self.__class__.__name__, - ) - - asset = context["asset"]["name"] - subset = context["subset"]["name"] - instance_name = pype.blender.plugin.rig_name(asset, subset, namespace) - - return self._get_instance_collection(instance_name, nodes) - def update(self, container: Dict, representation: Dict): """Update the loaded asset. From 2b6e90ffe00a44a7f9e972389a50c3c8b20fb972 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 10 Feb 2020 14:55:04 +0000 Subject: [PATCH 048/327] Creation, loading, and management of animations --- .../blender/create/create_animation.py | 30 ++ pype/plugins/blender/load/load_animation.py | 274 ++++++++++++++++++ .../blender/publish/collect_animation.py | 53 ++++ .../blender/publish/extract_animation.py | 47 +++ 4 files changed, 404 insertions(+) create mode 100644 pype/plugins/blender/create/create_animation.py create mode 100644 pype/plugins/blender/load/load_animation.py create mode 100644 pype/plugins/blender/publish/collect_animation.py create mode 100644 pype/plugins/blender/publish/extract_animation.py diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py new file mode 100644 index 0000000000..cfe569f918 --- /dev/null +++ b/pype/plugins/blender/create/create_animation.py @@ -0,0 +1,30 @@ +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateAnimation(Creator): + """Animation output for character rigs""" + + name = "animationMain" + label = "Animation" + family = "animation" + icon = "male" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py new file mode 100644 index 0000000000..5b527e1717 --- /dev/null +++ b/pype/plugins/blender/load/load_animation.py @@ -0,0 +1,274 @@ +"""Load an animation in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + + +class BlendAnimationLoader(pype.blender.AssetLoader): + """Load animations from a .blend file. + + Because they come from a .blend file we can simply link the collection that + contains the model. There is no further need to 'containerise' it. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["animation"] + representations = ["blend"] + + label = "Link Animation" + icon = "code-fork" + color = "orange" + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + # Get the armature of the rig + armatures = [obj for obj in collection_metadata["objects"] + if obj.type == 'ARMATURE'] + assert(len(armatures) == 1) + + for obj in collection_metadata["objects"]: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + objects_list = [] + + assert(len(armatures) == 1) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py new file mode 100644 index 0000000000..9bc0b02227 --- /dev/null +++ b/pype/plugins/blender/publish/collect_animation.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectAnimation(pyblish.api.ContextPlugin): + """Collect the data of an animation.""" + + hosts = ["blender"] + label = "Collect Animation" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_animation_collections() -> Generator: + """Return all 'animation' collections. + + Check if the family is 'animation' and if it doesn't have the + representation set. If the representation is set, it is a loaded rig + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'animation' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the animations from the current Blender scene.""" + collections = self.get_animation_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/extract_animation.py b/pype/plugins/blender/publish/extract_animation.py new file mode 100644 index 0000000000..dbfe29af83 --- /dev/null +++ b/pype/plugins/blender/publish/extract_animation.py @@ -0,0 +1,47 @@ +import os +import avalon.blender.workio + +import pype.api + + +class ExtractAnimation(pype.api.Extractor): + """Extract as animation.""" + + label = "Animation" + hosts = ["blender"] + families = ["animation"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From 0a561382f2dcb716fc2c311c7b94ee712383ff26 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 12 Feb 2020 12:53:48 +0000 Subject: [PATCH 049/327] Fixed a problem where loaded assets were collected for publishing --- pype/plugins/blender/load/load_animation.py | 4 ++++ pype/plugins/blender/load/load_model.py | 4 ++++ pype/plugins/blender/load/load_rig.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 5b527e1717..58a0e94665 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -103,6 +103,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): objects_list.append(obj) + animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -226,6 +228,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): avalon_info.update({"container_name": collection.name}) objects_list.append(obj) + animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index cde4109a7c..40d6c3434c 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -96,6 +96,8 @@ class BlendModelLoader(pype.blender.AssetLoader): objects_list.append(obj) + model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -204,6 +206,8 @@ class BlendModelLoader(pype.blender.AssetLoader): avalon_info.update({"container_name": collection.name}) objects_list.append(obj) + model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 361850c51b..c19717cd82 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -103,6 +103,8 @@ class BlendRigLoader(pype.blender.AssetLoader): objects_list.append(obj) + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -232,6 +234,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) From 09c9f66e4c79c1d7ed4b5185c912647be8e0825e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 19 Feb 2020 19:44:05 +0100 Subject: [PATCH 050/327] basic hook system --- pype/ftrack/lib/ftrack_app_handler.py | 5 +++++ pype/lib.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 9dc735987d..ea769ad167 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -273,6 +273,11 @@ class AppAction(BaseHandler): # Full path to executable launcher execfile = None + if application.get("launch_hook"): + hook = application.get("launch_hook") + self.log.info("launching hook: {}".format(hook)) + pypelib.execute_hook(application.get("launch_hook")) + if sys.platform == "win32": for ext in os.environ["PATHEXT"].split(os.pathsep): diff --git a/pype/lib.py b/pype/lib.py index 2235efa2f4..73bc16e97a 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -6,6 +6,8 @@ import contextlib import subprocess import inspect +import six + from avalon import io import avalon.api import avalon @@ -585,3 +587,21 @@ class CustomNone: def __repr__(self): """Representation of custom None.""" return "".format(str(self.identifier)) + + +def execute_hook(hook, **kwargs): + class_name = hook.split("/")[-1] + + abspath = os.path.join(os.getenv('PYPE_ROOT'), + 'repos', 'pype', **hook.split("/")[:-1]) + + try: + with open(abspath) as f: + six.exec_(f.read()) + + except Exception as exp: + log.exception("loading hook failed: {}".format(exp), + exc_info=True) + + hook_obj = globals()[class_name]() + hook_obj.execute(**kwargs) From 186b29340f33b56b774f8adf066a4794126a49d0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 14:13:19 +0100 Subject: [PATCH 051/327] added master version implementation to outdated check --- pype/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 2235efa2f4..796fe4f11f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -173,6 +173,8 @@ def is_latest(representation): """ version = io.find_one({"_id": representation['parent']}) + if version["type"] == "master_version": + return True # Get highest version under the parent highest_version = io.find_one({ From 6ff31dd9a232414b1537126b81caa669f4aea076 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 17:04:12 +0100 Subject: [PATCH 052/327] integrate_new also stores anatomy data to published_representations --- pype/plugins/global/publish/integrate_new.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index a2343ce8a9..18e492796a 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -255,6 +255,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if 'transfers' not in instance.data: instance.data['transfers'] = [] + published_representations = {} for idx, repre in enumerate(instance.data["representations"]): # create template data for Anatomy template_data = copy.deepcopy(anatomy_data) @@ -448,6 +449,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("__ destination_list: {}".format(destination_list)) instance.data['destination_list'] = destination_list representations.append(representation) + published_representations[repre_id] = { + "representation": representation, + "anatomy_data": template_data + } self.log.debug("__ representations: {}".format(representations)) # Remove old representations if there are any (before insertion of new) @@ -462,7 +467,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("__ represNAME: {}".format(rep['name'])) self.log.debug("__ represPATH: {}".format(rep['published_path'])) io.insert_many(representations) - instance.data["published_representations"] = representations + instance.data["published_representations"] = ( + published_representations + ) # self.log.debug("Representation: {}".format(representations)) self.log.info("Registered {} items".format(len(representations))) From ceac303221fdc96d66d16e2137ed44dc9e384bbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 17:04:50 +0100 Subject: [PATCH 053/327] integrate thumbnails do not raise error but log warnings --- pype/plugins/global/publish/integrate_thumbnail.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/integrate_thumbnail.py b/pype/plugins/global/publish/integrate_thumbnail.py index b623fa9072..5361c8aadb 100644 --- a/pype/plugins/global/publish/integrate_thumbnail.py +++ b/pype/plugins/global/publish/integrate_thumbnail.py @@ -21,14 +21,16 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): def process(self, instance): if not os.environ.get("AVALON_THUMBNAIL_ROOT"): - self.log.info("AVALON_THUMBNAIL_ROOT is not set." - " Skipping thumbnail integration.") + self.log.warning( + "AVALON_THUMBNAIL_ROOT is not set." + " Skipping thumbnail integration." + ) return published_repres = instance.data.get("published_representations") if not published_repres: self.log.debug( - "There are not published representation ids on the instance." + "There are not published representations on the instance." ) return @@ -36,10 +38,11 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] if "publish" not in anatomy.templates: - raise AssertionError("Anatomy does not have set publish key!") + self.warning("Anatomy does not have set publish key!") + return if "thumbnail" not in anatomy.templates["publish"]: - raise AssertionError(( + self.warning(( "There is not set \"thumbnail\" template for project \"{}\"" ).format(project_name)) From 20d6893e1dbaa19fcd9282ffccfc039896016222 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 17:05:48 +0100 Subject: [PATCH 054/327] integrate thumbnail uses new anatomy feature --- pype/plugins/global/publish/integrate_thumbnail.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pype/plugins/global/publish/integrate_thumbnail.py b/pype/plugins/global/publish/integrate_thumbnail.py index 5361c8aadb..78929713da 100644 --- a/pype/plugins/global/publish/integrate_thumbnail.py +++ b/pype/plugins/global/publish/integrate_thumbnail.py @@ -92,15 +92,9 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): }) anatomy_filled = anatomy.format(template_data) - final_path = anatomy_filled.get("publish", {}).get("thumbnail") - if not final_path: - raise AssertionError(( - "Anatomy template was not filled with entered data" - "\nTemplate: {} " - "\nData: {}" - ).format(thumbnail_template, str(template_data))) + template_filled = anatomy_filled["publish"]["thumbnail"] - dst_full_path = os.path.normpath(final_path) + dst_full_path = os.path.normpath(str(template_filled)) self.log.debug( "Copying file .. {} -> {}".format(src_full_path, dst_full_path) ) @@ -118,13 +112,14 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): template_data.pop("_id") template_data.pop("thumbnail_root") + repre_context = template_filled.used_values thumbnail_entity = { "_id": thumbnail_id, "type": "thumbnail", "schema": "pype:thumbnail-1.0", "data": { "template": thumbnail_template, - "template_data": template_data + "template_data": repre_context } } # Create thumbnail entity From edf48c01491568f4291de59d47370200cca32ac2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 17:06:57 +0100 Subject: [PATCH 055/327] added required keys for anatomy data to thumbnail context --- .../global/publish/integrate_thumbnail.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/integrate_thumbnail.py b/pype/plugins/global/publish/integrate_thumbnail.py index 78929713da..75755ccb64 100644 --- a/pype/plugins/global/publish/integrate_thumbnail.py +++ b/pype/plugins/global/publish/integrate_thumbnail.py @@ -18,6 +18,10 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.01 families = ["review"] + required_context_keys = [ + "project", "asset", "task", "subset", "version" + ] + def process(self, instance): if not os.environ.get("AVALON_THUMBNAIL_ROOT"): @@ -45,10 +49,7 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): self.warning(( "There is not set \"thumbnail\" template for project \"{}\"" ).format(project_name)) - - thumbnail_template = anatomy.templates["publish"]["thumbnail"] - - io.install() + return thumb_repre = None for repre in published_repres: @@ -62,6 +63,10 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ) return + io.install() + + thumbnail_template = anatomy.templates["publish"]["thumbnail"] + version = io.find_one({"_id": thumb_repre["parent"]}) if not version: raise AssertionError( @@ -83,7 +88,7 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): thumbnail_id = ObjectId() # Prepare anatomy template fill data - template_data = copy.deepcopy(thumb_repre["context"]) + template_data = copy.deepcopy(thumb_repre_anatomy_data) template_data.update({ "_id": str(thumbnail_id), "thumbnail_root": os.environ.get("AVALON_THUMBNAIL_ROOT"), @@ -113,6 +118,12 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): template_data.pop("thumbnail_root") repre_context = template_filled.used_values + for key in self.required_context_keys: + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] + thumbnail_entity = { "_id": thumbnail_id, "type": "thumbnail", From 4a3bf303d4170dceba4331ae9fdfe070f0ad5436 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 17:07:18 +0100 Subject: [PATCH 056/327] integrate thumbnails use new structure of published representations --- pype/plugins/global/publish/integrate_thumbnail.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_thumbnail.py b/pype/plugins/global/publish/integrate_thumbnail.py index 75755ccb64..0bb34eab58 100644 --- a/pype/plugins/global/publish/integrate_thumbnail.py +++ b/pype/plugins/global/publish/integrate_thumbnail.py @@ -52,9 +52,12 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): return thumb_repre = None - for repre in published_repres: + thumb_repre_anatomy_data = None + for repre_info in published_repres.values(): + repre = repre_info["representation"] if repre["name"].lower() == "thumbnail": thumb_repre = repre + thumb_repre_anatomy_data = repre_info["anatomy_data"] break if not thumb_repre: From 69c396ec3d3c3a3f5dbbfb9678e60e876bdc5a0e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 20 Feb 2020 17:35:38 +0100 Subject: [PATCH 057/327] polishing hooks --- pype/ftrack/lib/ftrack_app_handler.py | 8 ---- pype/hooks/unreal/unreal_prelaunch.py | 8 ++++ pype/lib.py | 54 +++++++++++++++++++++----- res/app_icons/ue4.png | Bin 0 -> 46503 bytes 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 pype/hooks/unreal/unreal_prelaunch.py create mode 100644 res/app_icons/ue4.png diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index ea769ad167..825a0a1985 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -256,14 +256,6 @@ class AppAction(BaseHandler): env = acre.merge(env, current_env=dict(os.environ)) env = acre.append(dict(os.environ), env) - - # - # tools_env = acre.get_tools(tools) - # env = acre.compute(dict(tools_env)) - # env = acre.merge(env, dict(os.environ)) - # os.environ = acre.append(dict(os.environ), env) - # os.environ = acre.compute(os.environ) - # Get path to execute st_temp_path = os.environ['PYPE_CONFIG'] os_plat = platform.system().lower() diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py new file mode 100644 index 0000000000..05d95a0b2a --- /dev/null +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -0,0 +1,8 @@ +from pype.lib import PypeHook + + +class UnrealPrelaunch(PypeHook): + + def execute(**kwargs): + print("I am inside!!!") + pass diff --git a/pype/lib.py b/pype/lib.py index 73bc16e97a..d1062e468f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1,10 +1,13 @@ import os +import sys +import types import re import logging import itertools import contextlib import subprocess import inspect +from abc import ABCMeta, abstractmethod import six @@ -123,7 +126,8 @@ def modified_environ(*remove, **update): is sure to work in all situations. :param remove: Environment variables to remove. - :param update: Dictionary of environment variables and values to add/update. + :param update: Dictionary of environment variables + and values to add/update. """ env = os.environ update = update or {} @@ -347,8 +351,8 @@ def switch_item(container, "parent": version["_id"]} ) - assert representation, ("Could not find representation in the database with" - " the name '%s'" % representation_name) + assert representation, ("Could not find representation in the database " + "with the name '%s'" % representation_name) avalon.api.switch(container, representation) @@ -481,7 +485,9 @@ def get_subsets(asset_name, """ Query subsets with filter on name. - The method will return all found subsets and its defined version and subsets. Version could be specified with number. Representation can be filtered. + The method will return all found subsets and its defined version + and subsets. Version could be specified with number. Representation + can be filtered. Arguments: asset_name (str): asset (shot) name @@ -498,8 +504,8 @@ def get_subsets(asset_name, asset_io = io.find_one({"type": "asset", "name": asset_name}) # check if anything returned - assert asset_io, "Asset not existing. \ - Check correct name: `{}`".format(asset_name) + assert asset_io, ( + "Asset not existing. Check correct name: `{}`").format(asset_name) # create subsets query filter filter_query = {"type": "subset", "parent": asset_io["_id"]} @@ -513,7 +519,9 @@ def get_subsets(asset_name, # query all assets subsets = [s for s in io.find(filter_query)] - assert subsets, "No subsets found. Check correct filter. Try this for start `r'.*'`: asset: `{}`".format(asset_name) + assert subsets, ("No subsets found. Check correct filter. " + "Try this for start `r'.*'`: " + "asset: `{}`").format(asset_name) output_dict = {} # Process subsets @@ -593,15 +601,43 @@ def execute_hook(hook, **kwargs): class_name = hook.split("/")[-1] abspath = os.path.join(os.getenv('PYPE_ROOT'), - 'repos', 'pype', **hook.split("/")[:-1]) + 'repos', 'pype', *hook.split("/")[:-1]) + + mod_name, mod_ext = os.path.splitext(os.path.basename(abspath)) + + if not mod_ext == ".py": + return + + module = types.ModuleType(mod_name) + module.__file__ = abspath + + log.info("-" * 80) + print(module) try: with open(abspath) as f: - six.exec_(f.read()) + six.exec_(f.read(), module.__dict__) + + sys.modules[abspath] = module except Exception as exp: log.exception("loading hook failed: {}".format(exp), exc_info=True) + from pprint import pprint + print("-" * 80) + pprint(dir(module)) + hook_obj = globals()[class_name]() hook_obj.execute(**kwargs) + + +@six.add_metaclass(ABCMeta) +class PypeHook: + + def __init__(self): + pass + + @abstractmethod + def execute(**kwargs): + pass diff --git a/res/app_icons/ue4.png b/res/app_icons/ue4.png new file mode 100644 index 0000000000000000000000000000000000000000..39201de6643767b8b24c821316b910e2bd41b1ba GIT binary patch literal 46503 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
>Di8y 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

r@KoO^&9yCj+{ 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 opy3f*F#kPbd<=|71&O Date: Thu, 20 Feb 2020 18:29:05 +0100 Subject: [PATCH 058/327] store more information into published repres --- pype/plugins/global/publish/integrate_new.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 18e492796a..fe2bcbff33 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -234,6 +234,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): bulk_writes ) + version = io.find_one({"_id": version_id}) + existing_repres = list(io.find({ "parent": version_id, "type": "archived_representation" @@ -451,7 +453,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): representations.append(representation) published_representations[repre_id] = { "representation": representation, - "anatomy_data": template_data + "anatomy_data": template_data, + # TODO prabably should store subset and version to instance + "subset_entity": subset, + "version_entity": version } self.log.debug("__ representations: {}".format(representations)) From 2abe39ef9d8f6e75aa92a3d145f909aeba4d8c16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 18:29:19 +0100 Subject: [PATCH 059/327] initial commit for instegrate master version --- .../publish/integrate_master_version.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 pype/plugins/global/publish/integrate_master_version.py diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py new file mode 100644 index 0000000000..efd01dd07c --- /dev/null +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -0,0 +1,129 @@ +import os +import logging +import shutil + +import errno +import pyblish.api +from avalon import api, io +from avalon.vendor import filelink + + +log = logging.getLogger(__name__) + + +class IntegrateMasterVersion(pyblish.api.InstancePlugin): + label = "Integrate Master Version" + # Must happen after IntegrateNew + order = pyblish.api.IntegratorOrder + 0.1 + + ignored_representation_names = [] + + def process(self, instance): + published_repres = instance.data.get("published_representations") + if not published_repres: + self.log.debug( + "There are not published representations on the instance." + ) + return + + project_name = api.Session["AVALON_PROJECT"] + + # TODO raise error if master not set? + anatomy = instance.context.data["anatomy"] + if "publish" not in anatomy.templates: + self.warning("Anatomy does not have set publish key!") + return + + if "master" not in anatomy.templates["publish"]: + self.warning(( + "There is not set \"master\" template for project \"{}\"" + ).format(project_name)) + return + + version_entity = None + + filtered_repre_ids = [] + for repre_id, repre_info in published_repres.items(): + repre = repre_info["representation"] + if version_entity is None: + version_entity = repre_info.get("version_entity") + + if repre["name"].lower() in self.ignored_representation_names: + filtered_repre_ids.append(repre_id) + + for repre_id in filtered_repre_ids: + published_repres.pop(repre_id, None) + + if not published_repres: + self.log.debug( + "All published representations were filtered by name." + ) + return + + if version_entity is None: + version_entity = ( + self.version_from_representations(published_repres) + ) + + if not version_entity: + self.log.warning("Can't find origin version in database.") + return + + cur_master_version, cur_master_repres = ( + self.current_master_ents(version_entity) + ) + + cur_master_repres_by_name = { + repre["name"].lower(): repre for repre in cur_master_repres + } + + if cur_master_version: + cur_master_version_id = cur_master_version["_id"] + else: + cur_master_version_id = io.ObjectId() + + new_master_version = { + "_id": cur_master_version_id, + "version_id": version_entity["_id"], + "parent": version_entity["parent"], + "type": "master_version", + "schema": "pype:master_version-1.0" + } + + repres_to_replace = {} + for repre_id, repre_info in published_repres.items(): + repre = repre_info["representation"] + repre_name_low = repre["name"].lower() + if repre_name_low in cur_master_repres_by_name: + repres_to_replace[repre_id] = ( + cur_master_repres_by_name.pop(repre_name_low) + ) + + if cur_master_version: + io.replace_one( + {"_id": new_master_version["_id"]}, + new_master_version + ) + else: + io.insert_one(new_master_version) + + def version_from_representations(self, repres): + for repre in repres: + version = io.find_one({"_id": repre["parent"]}) + if version: + return version + + def current_master_ents(self, version): + master_version = io.find_one({ + "parent": version["parent"], + "type": "master_version" + }) + + if not master_version: + return (None, []) + + master_repres = list(io.find({ + "parent": master_version["_id"], + "type": "representation" + })) + return (master_version, master_repres) From c06a4c337beb85df6e3a2bb18538ccb8a36c3f35 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Feb 2020 18:59:30 +0100 Subject: [PATCH 060/327] initial master version schema --- schema/master_version-1.0.json | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 schema/master_version-1.0.json diff --git a/schema/master_version-1.0.json b/schema/master_version-1.0.json new file mode 100644 index 0000000000..173a076537 --- /dev/null +++ b/schema/master_version-1.0.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + + "title": "pype:master_version-1.0", + "description": "Master version of asset", + + "type": "object", + + "additionalProperties": true, + + "required": [ + "schema", + "type", + "parent" + ], + + "properties": { + "_id": { + "description": "Document's id (database will create it's if not entered)", + "type": "ObjectId", + "example": "592c33475f8c1b064c4d1696" + }, + "schema": { + "description": "The schema associated with this document", + "type": "string", + "enum": ["avalon-core:master_version-3.0", "pype:master_version-3.0"], + "example": "pype:master_version-3.0" + }, + "type": { + "description": "The type of document", + "type": "string", + "enum": ["master_version"], + "example": "master_version" + }, + "parent": { + "description": "Unique identifier to parent document", + "type": "ObjectId", + "example": "592c33475f8c1b064c4d1696" + } + } +} From 6dfb258151f31e3c666a3a1b5a429c5c0905c3cb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Feb 2020 11:36:33 +0100 Subject: [PATCH 061/327] unreal project creation and launching --- pype/ftrack/lib/ftrack_app_handler.py | 9 +- pype/hooks/unreal/unreal_prelaunch.py | 80 ++++++- pype/lib.py | 33 ++- pype/unreal/__init__.py | 0 pype/unreal/lib.py | 305 ++++++++++++++++++++++++++ 5 files changed, 411 insertions(+), 16 deletions(-) create mode 100644 pype/unreal/__init__.py create mode 100644 pype/unreal/lib.py diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 825a0a1985..5dd33c1492 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -268,7 +268,14 @@ class AppAction(BaseHandler): if application.get("launch_hook"): hook = application.get("launch_hook") self.log.info("launching hook: {}".format(hook)) - pypelib.execute_hook(application.get("launch_hook")) + ret_val = pypelib.execute_hook( + application.get("launch_hook"), env=env) + if not ret_val: + return { + 'success': False, + 'message': "Hook didn't finish successfully {0}" + .format(self.label) + } if sys.platform == "win32": diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index 05d95a0b2a..83ba4bf8aa 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -1,8 +1,82 @@ +import logging +import os + from pype.lib import PypeHook +from pype.unreal import lib as unreal_lib +from pypeapp import Logger + +log = logging.getLogger(__name__) class UnrealPrelaunch(PypeHook): + """ + This hook will check if current workfile path has Unreal + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Unreal launcher + shell script. + """ - def execute(**kwargs): - print("I am inside!!!") - pass + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + if not env: + env = os.environ + asset = env["AVALON_ASSET"] + task = env["AVALON_TASK"] + workdir = env["AVALON_WORKDIR"] + engine_version = env["AVALON_APP_NAME"].split("_")[-1] + project_name = f"{asset}_{task}" + + # Unreal is sensitive about project names longer then 20 chars + if len(project_name) > 20: + self.log.warning((f"Project name exceed 20 characters " + f"[ {project_name} ]!")) + + # Unreal doesn't accept non alphabet characters at the start + # of the project name. This is because project name is then used + # in various places inside c++ code and there variable names cannot + # start with non-alpha. We append 'P' before project name to solve it. + # :scream: + if not project_name[:1].isalpha(): + self.log.warning(f"Project name doesn't start with alphabet " + f"character ({project_name}). Appending 'P'") + project_name = f"P{project_name}" + + project_path = os.path.join(workdir, project_name) + + self.log.info((f"{self.signature} requested UE4 version: " + f"[ {engine_version} ]")) + + detected = unreal_lib.get_engine_versions() + detected_str = ', '.join(detected.keys()) or 'none' + self.log.info((f"{self.signature} detected UE4 versions: " + f"[ {detected_str} ]")) + del(detected_str) + engine_version = ".".join(engine_version.split(".")[:2]) + if engine_version not in detected.keys(): + self.log.error((f"{self.signature} requested version not " + f"detected [ {engine_version} ]")) + return False + + os.makedirs(project_path, exist_ok=True) + + project_file = os.path.join(project_path, f"{project_name}.uproject") + if not os.path.isfile(project_file): + self.log.info((f"{self.signature} creating unreal " + f"project [ {project_name} ]")) + if env.get("AVALON_UNREAL_PLUGIN"): + os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501 + unreal_lib.create_unreal_project(project_name, + engine_version, project_path) + + self.log.info((f"{self.signature} preparing unreal project ... ")) + unreal_lib.prepare_project(project_file, detected[engine_version]) + + env["PYPE_UNREAL_PROJECT_FILE"] = project_file + return True diff --git a/pype/lib.py b/pype/lib.py index d1062e468f..87c206e758 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -597,7 +597,20 @@ class CustomNone: return "".format(str(self.identifier)) -def execute_hook(hook, **kwargs): +def execute_hook(hook, *args, **kwargs): + """ + This will load hook file, instantiate class and call `execute` method + on it. Hook must be in a form: + + `$PYPE_ROOT/repos/pype/path/to/hook.py/HookClass` + + This will load `hook.py`, instantiate HookClass and then execute_hook + `execute(*args, **kwargs)` + + :param hook: path to hook class + :type hook: str + """ + class_name = hook.split("/")[-1] abspath = os.path.join(os.getenv('PYPE_ROOT'), @@ -606,14 +619,11 @@ def execute_hook(hook, **kwargs): mod_name, mod_ext = os.path.splitext(os.path.basename(abspath)) if not mod_ext == ".py": - return + return False module = types.ModuleType(mod_name) module.__file__ = abspath - log.info("-" * 80) - print(module) - try: with open(abspath) as f: six.exec_(f.read(), module.__dict__) @@ -623,13 +633,12 @@ def execute_hook(hook, **kwargs): except Exception as exp: log.exception("loading hook failed: {}".format(exp), exc_info=True) + return False - from pprint import pprint - print("-" * 80) - pprint(dir(module)) - - hook_obj = globals()[class_name]() - hook_obj.execute(**kwargs) + obj = getattr(module, class_name) + hook_obj = obj() + ret_val = hook_obj.execute(*args, **kwargs) + return ret_val @six.add_metaclass(ABCMeta) @@ -639,5 +648,5 @@ class PypeHook: pass @abstractmethod - def execute(**kwargs): + def execute(self, *args, **kwargs): pass diff --git a/pype/unreal/__init__.py b/pype/unreal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py new file mode 100644 index 0000000000..8217e834a3 --- /dev/null +++ b/pype/unreal/lib.py @@ -0,0 +1,305 @@ +import os +import platform +import json +from distutils import dir_util +import subprocess + + +def get_engine_versions(): + """ + This will try to detect location and versions of installed Unreal Engine. + Location can be overridden by `UNREAL_ENGINE_LOCATION` environment + variable. + + Returns dictionary with version as a key and dir as value. + """ + try: + engine_locations = {} + root, dirs, files = next(os.walk(os.environ["UNREAL_ENGINE_LOCATION"])) + + for dir in dirs: + if dir.startswith("UE_"): + ver = dir.split("_")[1] + engine_locations[ver] = os.path.join(root, dir) + except KeyError: + # environment variable not set + pass + except OSError: + # specified directory doesn't exists + pass + + # if we've got something, terminate autodetection process + if engine_locations: + return engine_locations + + # else kick in platform specific detection + if platform.system().lower() == "windows": + return _win_get_engine_versions() + elif platform.system().lower() == "linux": + # on linux, there is no installation and getting Unreal Engine involves + # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. + pass + elif platform.system().lower() == "darwin": + return _darwin_get_engine_version() + + return {} + + +def _win_get_engine_versions(): + """ + If engines are installed via Epic Games Launcher then there is: + `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` + This file is JSON file listing installed stuff, Unreal engines + are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` + """ + install_json_path = os.path.join( + os.environ.get("PROGRAMDATA"), + "Epic", + "UnrealEngineLauncher", + "LauncherInstalled.dat", + ) + + return _parse_launcher_locations(install_json_path) + + +def _darwin_get_engine_version(): + """ + It works the same as on Windows, just JSON file location is different. + """ + install_json_path = os.path.join( + os.environ.get("HOME"), + "Library", + "Application Support", + "Epic", + "UnrealEngineLauncher", + "LauncherInstalled.dat", + ) + + return _parse_launcher_locations(install_json_path) + + +def _parse_launcher_locations(install_json_path): + engine_locations = {} + if os.path.isfile(install_json_path): + with open(install_json_path, "r") as ilf: + try: + install_data = json.load(ilf) + except json.JSONDecodeError: + raise Exception( + "Invalid `LauncherInstalled.dat file. `" + "Cannot determine Unreal Engine location." + ) + + for installation in install_data.get("InstallationList", []): + if installation.get("AppName").startswith("UE_"): + ver = installation.get("AppName").split("_")[1] + engine_locations[ver] = installation.get("InstallLocation") + + return engine_locations + + +def create_unreal_project(project_name, ue_version, dir): + """ + This will create `.uproject` file at specified location. As there is no + way I know to create project via command line, this is easiest option. + Unreal project file is basically JSON file. If we find + `AVALON_UNREAL_PLUGIN` environment variable we assume this is location + of Avalon Integration Plugin and we copy its content to project folder + and enable this plugin. + """ + + if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): + # copy plugin to correct path under project + plugin_path = os.path.join(dir, "Plugins", "Avalon") + if not os.path.isdir(plugin_path): + os.makedirs(plugin_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), + plugin_path) + + data = { + "FileVersion": 3, + "EngineAssociation": ue_version, + "Category": "", + "Description": "", + "Modules": [ + { + "Name": project_name, + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": ["Engine"], + } + ], + "Plugins": [ + {"Name": "PythonScriptPlugin", "Enabled": True}, + {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "Avalon", "Enabled": True}, + ], + } + + project_file = os.path.join(dir, "{}.uproject".format(project_name)) + with open(project_file, mode="w") as pf: + json.dump(data, pf, indent=4) + + +def prepare_project(project_file: str, engine_path: str): + """ + This function will add source files needed for project to be + rebuild along with the avalon integration plugin. + + There seems not to be automated way to do it from command line. + But there might be way to create at least those target and build files + by some generator. This needs more research as manually writing + those files is rather hackish. :skull_and_crossbones: + + :param project_file: path to .uproject file + :type project_file: str + :param engine_path: path to unreal engine associated with project + :type engine_path: str + """ + + project_name = os.path.splitext(os.path.basename(project_file))[0] + project_dir = os.path.dirname(project_file) + targets_dir = os.path.join(project_dir, "Source") + sources_dir = os.path.join(targets_dir, project_name) + + os.makedirs(sources_dir, exist_ok=True) + os.makedirs(os.path.join(project_dir, "Content"), exist_ok=True) + + module_target = ''' +using UnrealBuildTool; +using System.Collections.Generic; + +public class {0}Target : TargetRules +{{ + public {0}Target( TargetInfo Target) : base(Target) + {{ + Type = TargetType.Game; + ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); + }} +}} +'''.format(project_name) + + editor_module_target = ''' +using UnrealBuildTool; +using System.Collections.Generic; + +public class {0}EditorTarget : TargetRules +{{ + public {0}EditorTarget( TargetInfo Target) : base(Target) + {{ + Type = TargetType.Editor; + + ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); + }} +}} +'''.format(project_name) + + module_build = ''' +using UnrealBuildTool; +public class {0} : ModuleRules +{{ + public {0}(ReadOnlyTargetRules Target) : base(Target) + {{ + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new string[] {{ "Core", + "CoreUObject", "Engine", "InputCore" }}); + PrivateDependencyModuleNames.AddRange(new string[] {{ }}); + }} +}} +'''.format(project_name) + + module_cpp = ''' +#include "{0}.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, {0}, "{0}" ); +'''.format(project_name) + + module_header = ''' +#pragma once +#include "CoreMinimal.h" +''' + + game_mode_cpp = ''' +#include "{0}GameModeBase.h" +'''.format(project_name) + + game_mode_h = ''' +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "{0}GameModeBase.generated.h" + +UCLASS() +class {1}_API A{0}GameModeBase : public AGameModeBase +{{ + GENERATED_BODY() +}}; +'''.format(project_name, project_name.upper()) + + with open(os.path.join( + targets_dir, f"{project_name}.Target.cs"), mode="w") as f: + f.write(module_target) + + with open(os.path.join( + targets_dir, f"{project_name}Editor.Target.cs"), mode="w") as f: + f.write(editor_module_target) + + with open(os.path.join( + sources_dir, f"{project_name}.Build.cs"), mode="w") as f: + f.write(module_build) + + with open(os.path.join( + sources_dir, f"{project_name}.cpp"), mode="w") as f: + f.write(module_cpp) + + with open(os.path.join( + sources_dir, f"{project_name}.h"), mode="w") as f: + f.write(module_header) + + with open(os.path.join( + sources_dir, f"{project_name}GameModeBase.cpp"), mode="w") as f: + f.write(game_mode_cpp) + + with open(os.path.join( + sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: + f.write(game_mode_h) + + if platform.system().lower() == "windows": + u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" + "UnrealBuildTool.exe") + u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" + f"UnrealHeaderTool.exe") + elif platform.system().lower() == "linux": + # WARNING: there is no UnrealBuildTool on linux? + u_build_tool = "" + u_header_tool = "" + elif platform.system().lower() == "darwin": + # WARNING: there is no UnrealBuildTool on Mac? + u_build_tool = "" + u_header_tool = "" + + u_build_tool = u_build_tool.replace("\\", "/") + u_header_tool = u_header_tool.replace("\\", "/") + + command1 = [u_build_tool, "-projectfiles", f"-project={project_file}", + "-progress"] + + subprocess.run(command1) + + command2 = [u_build_tool, f"-ModuleWithSuffix={project_name},3555" + "Win64", "Development", "-TargetType=Editor" + f'-Project="{project_file}"', f'"{project_file}"' + "-IgnoreJunk"] + + subprocess.run(command2) + + uhtmanifest = os.path.join(os.path.dirname(project_file), + f"{project_name}.uhtmanifest") + + command3 = [u_header_tool, f'"{project_file}"', f'"{uhtmanifest}"', + "-Unattended", "-WarningsAsErrors", "-installed"] + + subprocess.run(command3) From 34438fcc42a47076ae7bc089eebb99fa02c081f0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 21 Feb 2020 19:17:29 +0100 Subject: [PATCH 062/327] seems to look like it may work once --- .../publish/integrate_master_version.py | 260 ++++++++++++++++-- 1 file changed, 231 insertions(+), 29 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index efd01dd07c..6991978a24 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -1,10 +1,10 @@ import os +import copy import logging -import shutil -import errno +from pymongo import InsertOne, ReplaceOne import pyblish.api -from avalon import api, io +from avalon import api, io, pipeline from avalon.vendor import filelink @@ -40,13 +40,15 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ).format(project_name)) return - version_entity = None + master_template = anatomy.templates["publish"]["master"] + + src_version_entity = None filtered_repre_ids = [] for repre_id, repre_info in published_repres.items(): repre = repre_info["representation"] - if version_entity is None: - version_entity = repre_info.get("version_entity") + if src_version_entity is None: + src_version_entity = repre_info.get("version_entity") if repre["name"].lower() in self.ignored_representation_names: filtered_repre_ids.append(repre_id) @@ -60,52 +62,252 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ) return - if version_entity is None: - version_entity = ( + if src_version_entity is None: + src_version_entity = ( self.version_from_representations(published_repres) ) - if not version_entity: + if not src_version_entity: self.log.warning("Can't find origin version in database.") return - cur_master_version, cur_master_repres = ( - self.current_master_ents(version_entity) + old_version, old_repres = ( + self.current_master_ents(src_version_entity) ) - cur_master_repres_by_name = { - repre["name"].lower(): repre for repre in cur_master_repres + old_repres_by_name = { + repre["name"].lower(): repre for repre in old_repres } - if cur_master_version: - cur_master_version_id = cur_master_version["_id"] + if old_version: + new_version_id = old_version["_id"] else: - cur_master_version_id = io.ObjectId() + new_version_id = io.ObjectId() new_master_version = { - "_id": cur_master_version_id, - "version_id": version_entity["_id"], - "parent": version_entity["parent"], + "_id": new_version_id, + "version_id": src_version_entity["_id"], + "parent": src_version_entity["parent"], "type": "master_version", "schema": "pype:master_version-1.0" } - repres_to_replace = {} + bulk_writes = [] + + if old_version: + bulk_writes.append( + ReplaceOne( + {"_id": new_master_version["_id"]}, + new_master_version + ) + ) + else: + bulk_writes.append( + InsertOne(new_master_version) + ) + + # Separate old representations into `to replace` and `to delete` + old_repres_to_replace = {} + old_repres_to_delete = {} for repre_id, repre_info in published_repres.items(): repre = repre_info["representation"] repre_name_low = repre["name"].lower() - if repre_name_low in cur_master_repres_by_name: - repres_to_replace[repre_id] = ( - cur_master_repres_by_name.pop(repre_name_low) + if repre_name_low in old_repres_by_name: + old_repres_to_replace[repre_name_low] = ( + old_repres_by_name.pop(repre_name_low) + ) + else: + old_repres_to_delete[repre_name_low] = ( + old_repres_by_name.pop(repre_name_low) ) - if cur_master_version: - io.replace_one( - {"_id": new_master_version["_id"]}, - new_master_version + archived_repres = list(io.find({ + # Check what is type of archived representation + "type": "archived_repsentation", + "parent": new_version_id + })) + archived_repres_by_name = {} + for repre in archived_repres: + repre_name_low = repre["name"].lower() + archived_repres_by_name[repre_name_low] = repre + + self.delete_repre_files(old_repres) + + for repre_id, repre_info in published_repres.items(): + repre = copy.deepcopy(repre_info["representation"]) + repre_name_low = repre["name"].lower() + + repre["parent"] = new_master_version["_id"] + # TODO change repre data and context (new anatomy) + # TODO hardlink files + + # Replace current representation + if repre_name_low in old_repres_to_replace: + old_repre = old_repres_to_replace.pop(repre_name_low) + repre["_id"] = old_repre["_id"] + bulk_writes.append( + ReplaceOne( + {"_id": old_repre["_id"]}, + repre + ) + ) + + # Unarchive representation + elif repre_name_low in archived_repres_by_name: + archived_repre = archived_repres_by_name.pop(repre_name_low) + old_id = archived_repre["old_id"] + repre["_id"] = old_id + bulk_writes.append( + ReplaceOne( + {"old_id": old_id}, + repre + ) + ) + + # Create representation + else: + repre["_id"] = io.ObjectId() + bulk_writes.append( + InsertOne(repre) + ) + + # Archive not replaced old representations + for repre_name_low, repre in old_repres_to_delete.items(): + # TODO delete their files + + # Replace archived representation (This is backup) + # - should not happen to have both repre and archived repre + if repre_name_low in archived_repres_by_name: + archived_repre = archived_repres_by_name.pop(repre_name_low) + repre["old_id"] = repre["_id"] + repre["_id"] = archived_repre["_id"] + repre["type"] = archived_repre["type"] + bulk_writes.append( + ReplaceOne( + {"_id": archived_repre["_id"]}, + repre + ) + ) + + else: + repre["old_id"] = repre["_id"] + repre["_id"] = io.ObjectId() + repre["type"] = "archived_representation" + bulk_writes.append( + InsertOne(repre) + ) + + if bulk_writes: + pass + + def delete_repre_files(self, repres): + if not repres: + return + + frame_splitter = "_-_FRAME_-_" + files_to_delete = [] + for repre in repres: + is_sequence = False + if "frame" in repre["context"]: + repre["context"]["frame"] = frame_splitter + is_sequence = True + + template = repre["data"]["template"] + context = repre["context"] + context["root"] = api.registered_root() + path = pipeline.format_template_with_optional_keys( + context, template ) - else: - io.insert_one(new_master_version) + path = os.path.normpath(path) + if not is_sequence: + if os.path.exists(path): + files_to_delete.append(path) + continue + + dirpath = os.path.dirname(path) + file_start = None + file_end = None + file_items = path.split(frame_splitter) + if len(file_items) == 0: + continue + elif len(file_items) == 1: + if path.startswith(frame_splitter): + file_end = file_items[0] + else: + file_start = file_items[1] + + elif len(file_items) == 2: + file_start, file_end = file_items + + else: + raise ValueError(( + "Representation template has `frame` key " + "more than once inside." + )) + + for file_name in os.listdir(dirpath): + check_name = str(file_name) + if file_start and not check_name.startswith(file_start): + continue + check_name.replace(file_start, "") + + if file_end and not check_name.endswith(file_end): + continue + check_name.replace(file_end, "") + + # File does not have frame + if not check_name: + continue + + files_to_delete.append(os.path.join(dirpath, file_name)) + + renamed_files = [] + failed = False + for file_path in files_to_delete: + # TODO too robust for testing - should be easier in future + _rename_path = file_path + ".BACKUP" + rename_path = None + max_index = 200 + cur_index = 1 + while True: + if max_index >= cur_index: + raise Exception(( + "Max while loop index reached! Can't make backup" + " for previous master version." + )) + break + + if not os.path.exists(_rename_path): + rename_path = _rename_path + break + + try: + os.remove(_rename_path) + except Exception: + _rename_path = file_path + ".BACKUP{}".format( + str(cur_index) + ) + cur_index += 1 + + try: + args = (file_path, rename_path) + os.rename(*args) + renamed_files.append(args) + except Exception: + failed = True + break + + if failed: + for dst_name, src_name in renamed_files: + os.rename(src_name, dst_name) + + raise AssertionError(( + "Could not create master version because it is not possible" + " to replace current master files." + )) + + for _, renamed_path in renamed_files: + os.remove(renamed_path) def version_from_representations(self, repres): for repre in repres: From 1537b2ba571e0e614277907377e30f303d4b48c4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Feb 2020 21:06:30 +0100 Subject: [PATCH 063/327] switch to UnrealEnginePython --- pype/hooks/unreal/unreal_prelaunch.py | 1 + pype/unreal/lib.py | 49 ++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index 83ba4bf8aa..cb3b6e8e64 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -79,4 +79,5 @@ class UnrealPrelaunch(PypeHook): unreal_lib.prepare_project(project_file, detected[engine_version]) env["PYPE_UNREAL_PROJECT_FILE"] = project_file + env["AVALON_CURRENT_UNREAL_ENGINE"] = detected[engine_version] return True diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 8217e834a3..6130b38764 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -1,3 +1,4 @@ +import sys import os import platform import json @@ -107,15 +108,52 @@ def create_unreal_project(project_name, ue_version, dir): of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. """ + import git if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugin_path = os.path.join(dir, "Plugins", "Avalon") - if not os.path.isdir(plugin_path): - os.makedirs(plugin_path, exist_ok=True) + plugins_path = os.path.join(dir, "Plugins") + avalon_plugin_path = os.path.join(plugins_path, "Avalon") + if not os.path.isdir(avalon_plugin_path): + os.makedirs(avalon_plugin_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), - plugin_path) + avalon_plugin_path) + + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. + # Otherwise clone UnrealEnginePython to Plugins directory + # https://github.com/20tab/UnrealEnginePython.git + uep_path = os.path.join(plugins_path, "UnrealEnginePython") + if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + + os.makedirs(uep_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree(os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + uep_path) + else: + git.Repo.clone_from("https://github.com/20tab/UnrealEnginePython.git", + uep_path) + + # now we need to fix python path in: + # `UnrealEnginePython.Build.cs` + # to point to our python + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="r") as f: + build_file = f.read() + + fix = build_file.replace( + 'private string pythonHome = "";', + 'private string pythonHome = "{}";'.format( + sys.base_prefix.replace("\\", "/"))) + + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="w") as f: + f.write(fix) data = { "FileVersion": 3, @@ -134,6 +172,7 @@ def create_unreal_project(project_name, ue_version, dir): {"Name": "PythonScriptPlugin", "Enabled": True}, {"Name": "EditorScriptingUtilities", "Enabled": True}, {"Name": "Avalon", "Enabled": True}, + {"Name": "UnrealEnginePython", "Enabled": True} ], } @@ -296,6 +335,7 @@ class {1}_API A{0}GameModeBase : public AGameModeBase subprocess.run(command2) + """ uhtmanifest = os.path.join(os.path.dirname(project_file), f"{project_name}.uhtmanifest") @@ -303,3 +343,4 @@ class {1}_API A{0}GameModeBase : public AGameModeBase "-Unattended", "-WarningsAsErrors", "-installed"] subprocess.run(command3) + """ From 36c35dbf8433be8600eb9041c0d9e4d9c2fc8953 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 14:06:41 +0100 Subject: [PATCH 064/327] store all published files per representation --- pype/plugins/global/publish/integrate_new.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index fe2bcbff33..8ef027bb93 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -259,6 +259,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): published_representations = {} for idx, repre in enumerate(instance.data["representations"]): + published_files = [] + # create template data for Anatomy template_data = copy.deepcopy(anatomy_data) if intent is not None: @@ -364,16 +366,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("source: {}".format(src)) instance.data["transfers"].append([src, dst]) + published_files.append(dst) + # for adding first frame into db if not dst_start_frame: dst_start_frame = dst_padding - dst = "{0}{1}{2}".format( - dst_head, - dst_start_frame, - dst_tail).replace("..", ".") - repre['published_path'] = self.unc_convert(dst) - else: # Single file # _______ @@ -402,9 +400,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) - repre['published_path'] = self.unc_convert(dst) + published_files.append(dst) + self.log.debug("__ dst: {}".format(dst)) + repre["publishedFiles"] = published_files + for key in self.db_representation_context_keys: value = template_data.get(key) if not value: @@ -454,6 +455,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): published_representations[repre_id] = { "representation": representation, "anatomy_data": template_data, + "published_files": published_files, # TODO prabably should store subset and version to instance "subset_entity": subset, "version_entity": version @@ -470,7 +472,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("__ representations: {}".format(representations)) for rep in instance.data["representations"]: self.log.debug("__ represNAME: {}".format(rep['name'])) - self.log.debug("__ represPATH: {}".format(rep['published_path'])) + self.log.debug("__ represPATH:\n{}".format( + ",\n".join(rep['publishedFiles']) + )) io.insert_many(representations) instance.data["published_representations"] = ( published_representations From 2a128b0956f23a03cc844b8d8e7fbf379c7ac7bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 14:06:58 +0100 Subject: [PATCH 065/327] added first version of file mapping --- .../publish/integrate_master_version.py | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 6991978a24..a93226ae18 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -1,6 +1,7 @@ import os import copy import logging +import clique from pymongo import InsertOne, ReplaceOne import pyblish.api @@ -40,8 +41,6 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ).format(project_name)) return - master_template = anatomy.templates["publish"]["master"] - src_version_entity = None filtered_repre_ids = [] @@ -133,13 +132,21 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): self.delete_repre_files(old_repres) - for repre_id, repre_info in published_repres.items(): - repre = copy.deepcopy(repre_info["representation"]) - repre_name_low = repre["name"].lower() + master_template = anatomy.templates["publish"]["master"] + src_to_dst_file_paths = [] + for repre_id, repre_info in published_repres.items(): + + # Skip if new repre does not have published repre files + published_files = repre_info["published_files"] + if len(published_files) == 0: + continue + + # Prepare new repre + repre = copy.deepcopy(repre_info["representation"]) repre["parent"] = new_master_version["_id"] - # TODO change repre data and context (new anatomy) - # TODO hardlink files + + repre_name_low = repre["name"].lower() # Replace current representation if repre_name_low in old_repres_to_replace: @@ -171,6 +178,52 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): InsertOne(repre) ) + # TODO change repre data and context (new anatomy) + # TODO hardlink files + + # Prepare anatomy data + anatomy_data = repre_info["anatomy_data"] + anatomy_data.pop("version", None) + + if len(published_files) == 1: + anatomy_filled = anatomy.format(anatomy_data) + template_filled = anatomy_filled["publish"]["master"] + src_to_dst_file_paths.append( + (published_files[0], template_filled) + ) + continue + + collections, remainders = clique.assemble(published_files) + if remainders or not collections or len(collections) > 1: + raise Exception(( + "Integrity error. Files of published representation" + " is combination of frame collections and single files." + )) + + src_col = collections[0] + + # Get filled path to repre context + anatomy_filled = anatomy.format(anatomy_data) + template_filled = anatomy_filled["publish"]["master"] + + # Get head and tail for collection + frame_splitter = "_-_FRAME_SPLIT_-_" + anatomy_data["frame"] = frame_splitter + _anatomy_filled = anatomy.format(anatomy_data) + _template_filled = _anatomy_filled["publish"]["master"] + head, tail = _template_filled.split(frame_splitter) + padding = ( + anatomy.templates["render"]["padding"] + ) + + dst_col = clique.Collection(head=head, padding=padding, tail=tail) + dst_col.indexes.clear() + dst_col.indexes.update(src_col.indexes) + for src_file, dst_file in zip(src_col, dst_col): + src_to_dst_file_paths.append( + (src_file, dst_file) + ) + # Archive not replaced old representations for repre_name_low, repre in old_repres_to_delete.items(): # TODO delete their files From 1a3463c78c5f10f013766991142bbd94708077d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 16:40:42 +0100 Subject: [PATCH 066/327] representation context and data are replaced with new data --- .../publish/integrate_master_version.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index a93226ae18..b508404d77 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -2,6 +2,7 @@ import os import copy import logging import clique +import errno from pymongo import InsertOne, ReplaceOne import pyblish.api @@ -18,6 +19,10 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.1 ignored_representation_names = [] + db_representation_context_keys = [ + "project", "asset", "task", "subset", "representation", + "family", "hierarchy", "task", "username" + ] def process(self, instance): published_repres = instance.data.get("published_representations") @@ -142,9 +147,34 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): if len(published_files) == 0: continue + # Prepare anatomy data + anatomy_data = repre_info["anatomy_data"] + anatomy_data.pop("version", None) + + # Get filled path to repre context + anatomy_filled = anatomy.format(anatomy_data) + template_filled = anatomy_filled["publish"]["master"] + + repre_data = { + "path": str(template_filled), + "template": master_template + } + repre_context = template_filled.used_values + for key in self.db_representation_context_keys: + if ( + key in repre_context or + key not in anatomy_data + ): + continue + + repre_context[key] = anatomy_data[key] + + # TODO change repre data and context (new anatomy) # Prepare new repre repre = copy.deepcopy(repre_info["representation"]) repre["parent"] = new_master_version["_id"] + repre["context"] = repre_context + repre["data"] = repre_data repre_name_low = repre["name"].lower() @@ -178,16 +208,8 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): InsertOne(repre) ) - # TODO change repre data and context (new anatomy) # TODO hardlink files - - # Prepare anatomy data - anatomy_data = repre_info["anatomy_data"] - anatomy_data.pop("version", None) - if len(published_files) == 1: - anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled["publish"]["master"] src_to_dst_file_paths.append( (published_files[0], template_filled) ) @@ -202,10 +224,6 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): src_col = collections[0] - # Get filled path to repre context - anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled["publish"]["master"] - # Get head and tail for collection frame_splitter = "_-_FRAME_SPLIT_-_" anatomy_data["frame"] = frame_splitter From 84dceb42afda2753a74f9e86b5b1aa10aa748b0a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 16:41:04 +0100 Subject: [PATCH 067/327] added reate hardlink and path root checker --- .../publish/integrate_master_version.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index b508404d77..1ec0bd00dd 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -242,6 +242,10 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): (src_file, dst_file) ) + # TODO should we *only* create hardlinks? + for src_path, dst_path in src_to_dst_file_paths: + self.create_hardlink(src_path, dst_path) + # Archive not replaced old representations for repre_name_low, repre in old_repres_to_delete.items(): # TODO delete their files @@ -271,6 +275,83 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): if bulk_writes: pass + def create_hardlink(self, src_path, dst_path): + dst_path = self.path_root_check(dst_path) + src_path = self.path_root_check(src_path) + + dirname = os.path.dirname(dst_path) + + try: + os.makedirs(dirname) + except OSError as exc: + if exc.errno != errno.EEXIST: + self.log.error("An unexpected error occurred.", exc_info=True) + raise + + filelink.create(src_path, dst_path, filelink.HARDLINK) + + def path_root_check(self, path): + normalized_path = os.path.normpath(path) + forward_slash_path = normalized_path.replace("\\", "/") + + drive, _path = os.path.splitdrive(normalized_path) + if os.path.exists(drive + "/"): + self.log.debug( + "Drive \"{}\" exist. Nothing to change.".format(drive) + ) + return normalized_path + + path_env_key = "PYPE_STUDIO_PROJECTS_PATH" + mount_env_key = "PYPE_STUDIO_PROJECTS_MOUNT" + missing_envs = [] + if path_env_key not in os.environ: + missing_envs.append(path_env_key) + + if mount_env_key not in os.environ: + missing_envs.append(mount_env_key) + + if missing_envs: + _add_s = "" + if len(missing_envs) > 1: + _add_s = "s" + + self.log.warning(( + "Can't replace MOUNT drive path to UNC path due to missing" + " environment variable{}: `{}`. This may cause issues during" + " publishing process." + ).format(_add_s, ", ".join(missing_envs))) + + return normalized_path + + unc_root = os.environ[path_env_key].replace("\\", "/") + mount_root = os.environ[mount_env_key].replace("\\", "/") + + # --- Remove slashes at the end of mount and unc roots --- + while unc_root.endswith("/"): + unc_root = unc_root[:-1] + + while mount_root.endswith("/"): + mount_root = mount_root[:-1] + # --- + + if forward_slash_path.startswith(unc_root): + self.log.debug(( + "Path already starts with UNC root: \"{}\"" + ).format(unc_root)) + return normalized_path + + if not forward_slash_path.startswith(mount_root): + self.log.warning(( + "Path do not start with MOUNT root \"{}\" " + "set in environment variable \"{}\"" + ).format(unc_root, mount_env_key)) + return normalized_path + + # Replace Mount root with Unc root + path = unc_root + forward_slash_path[len(mount_root):] + + return os.path.normpath(path) + def delete_repre_files(self, repres): if not repres: return From 12e95ef32c883f44f0b1136e3023065a97b469dc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 17:24:40 +0100 Subject: [PATCH 068/327] publihsed_path moved back due to integrity errors connected with removing --- pype/plugins/global/publish/integrate_new.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 779a498451..f8cde10aed 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -373,6 +373,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not dst_start_frame: dst_start_frame = dst_padding + dst = "{0}{1}{2}".format( + dst_head, + dst_start_frame, + dst_tail + ).replace("..", ".") + repre['published_path'] = self.unc_convert(dst) + else: # Single file # _______ @@ -402,7 +409,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) published_files.append(dst) - + repre['published_path'] = self.unc_convert(dst) self.log.debug("__ dst: {}".format(dst)) repre["publishedFiles"] = published_files @@ -473,9 +480,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.debug("__ representations: {}".format(representations)) for rep in instance.data["representations"]: self.log.debug("__ represNAME: {}".format(rep['name'])) - self.log.debug("__ represPATH:\n{}".format( - ",\n".join(rep['publishedFiles']) - )) + self.log.debug("__ represPATH: {}".format(rep['published_path'])) io.insert_many(representations) instance.data["published_representations"] = ( published_representations From 875ca5cd6fd4f0da67b2dba80897dd27b6505bda Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Feb 2020 16:26:10 +0000 Subject: [PATCH 069/327] Extraction for blend files is now handled by a single class --- .../blender/publish/collect_current_file.py | 2 + ...{extract_animation.py => extract_blend.py} | 93 +++++++++---------- pype/plugins/blender/publish/extract_model.py | 47 ---------- pype/plugins/blender/publish/extract_rig.py | 47 ---------- 4 files changed, 48 insertions(+), 141 deletions(-) rename pype/plugins/blender/publish/{extract_animation.py => extract_blend.py} (86%) delete mode 100644 pype/plugins/blender/publish/extract_model.py delete mode 100644 pype/plugins/blender/publish/extract_rig.py diff --git a/pype/plugins/blender/publish/collect_current_file.py b/pype/plugins/blender/publish/collect_current_file.py index a097c72047..926d290b31 100644 --- a/pype/plugins/blender/publish/collect_current_file.py +++ b/pype/plugins/blender/publish/collect_current_file.py @@ -14,3 +14,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file""" current_file = bpy.data.filepath context.data['currentFile'] = current_file + + assert current_file != '', "Current file is empty. Save the file before continuing." diff --git a/pype/plugins/blender/publish/extract_animation.py b/pype/plugins/blender/publish/extract_blend.py similarity index 86% rename from pype/plugins/blender/publish/extract_animation.py rename to pype/plugins/blender/publish/extract_blend.py index dbfe29af83..7e11e9ef8d 100644 --- a/pype/plugins/blender/publish/extract_animation.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -1,47 +1,46 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractAnimation(pype.api.Extractor): - """Extract as animation.""" - - label = "Animation" - hosts = ["blender"] - families = ["animation"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) +import os +import avalon.blender.workio + +import pype.api + + +class ExtractBlend(pype.api.Extractor): + """Extract a blend file.""" + + label = "Extract Blend" + hosts = ["blender"] + families = ["animation", "model", "rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_model.py b/pype/plugins/blender/publish/extract_model.py deleted file mode 100644 index 501c4d9d5c..0000000000 --- a/pype/plugins/blender/publish/extract_model.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractModel(pype.api.Extractor): - """Extract as model.""" - - label = "Model" - hosts = ["blender"] - families = ["model"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_rig.py b/pype/plugins/blender/publish/extract_rig.py deleted file mode 100644 index 8a3c83d07c..0000000000 --- a/pype/plugins/blender/publish/extract_rig.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractRig(pype.api.Extractor): - """Extract as rig.""" - - label = "Rig" - hosts = ["blender"] - families = ["rig"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From 22e8c301467f3f35e1d39daed855b646602c8633 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 17:26:17 +0100 Subject: [PATCH 070/327] fixed old repres to delete variable --- pype/plugins/global/publish/integrate_master_version.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 1ec0bd00dd..3df74f4e28 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -120,10 +120,9 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): old_repres_to_replace[repre_name_low] = ( old_repres_by_name.pop(repre_name_low) ) - else: - old_repres_to_delete[repre_name_low] = ( - old_repres_by_name.pop(repre_name_low) - ) + + if old_repres_by_name: + old_repres_to_delete = old_repres_by_name archived_repres = list(io.find({ # Check what is type of archived representation From f2733c0a1b05bff6afa7b21a00c84beb436ca31d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Feb 2020 16:26:40 +0000 Subject: [PATCH 071/327] Implemented extraction to FBX files --- pype/plugins/blender/publish/extract_fbx.py | 71 +++++++++++ .../blender/publish/extract_fbx_animation.py | 118 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 pype/plugins/blender/publish/extract_fbx.py create mode 100644 pype/plugins/blender/publish/extract_fbx_animation.py diff --git a/pype/plugins/blender/publish/extract_fbx.py b/pype/plugins/blender/publish/extract_fbx.py new file mode 100644 index 0000000000..95466c1d2b --- /dev/null +++ b/pype/plugins/blender/publish/extract_fbx.py @@ -0,0 +1,71 @@ +import os +import avalon.blender.workio + +import pype.api + +import bpy + +class ExtractFBX(pype.api.Extractor): + """Extract as FBX.""" + + label = "Extract FBX" + hosts = ["blender"] + families = ["model", "rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one collection collected for this asset" + + old_active_layer_collection = bpy.context.view_layer.active_layer_collection + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection (but there is the vice versa). + layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + bpy.context.view_layer.active_layer_collection = layer_collections[0] + + old_scale = bpy.context.scene.unit_settings.scale_length + + # We set the scale of the scene for the export + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=True, + mesh_smooth_type='FACE', + add_leaf_bones=False + ) + + bpy.context.view_layer.active_layer_collection = old_active_layer_collection + + bpy.context.scene.unit_settings.scale_length = old_scale + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py new file mode 100644 index 0000000000..bc088f8bb7 --- /dev/null +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -0,0 +1,118 @@ +import os +import avalon.blender.workio + +import pype.api + +import bpy +import bpy_extras +import bpy_extras.anim_utils + + +class ExtractAnimationFBX(pype.api.Extractor): + """Extract as animation.""" + + label = "Extract FBX" + hosts = ["blender"] + families = ["animation"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one collection collected for this asset" + + old_active_layer_collection = bpy.context.view_layer.active_layer_collection + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection (but there is the vice versa). + layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + bpy.context.view_layer.active_layer_collection = layer_collections[0] + + old_scale = bpy.context.scene.unit_settings.scale_length + + # We set the scale of the scene for the export + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export all the objects in the collection + objects_to_export = collections[0].objects + + object_action_pairs = [] + original_actions = [] + + starting_frames = [] + ending_frames = [] + + # For each object, we make a copy of the current action + for obj in objects_to_export: + + curr_action = obj.animation_data.action + copy_action = curr_action.copy() + + object_action_pairs.append((obj, copy_action)) + original_actions.append(curr_action) + + curr_frame_range = curr_action.frame_range + + starting_frames.append( curr_frame_range[0] ) + ending_frames.append( curr_frame_range[1] ) + + # We compute the starting and ending frames + max_frame = min( starting_frames ) + min_frame = max( ending_frames ) + + # We bake the copy of the current action for each object + bpy_extras.anim_utils.bake_action_objects( + object_action_pairs, + frames=range(int(min_frame), int(max_frame)), + do_object=False, + do_clean=False + ) + + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False + ) + + bpy.context.view_layer.active_layer_collection = old_active_layer_collection + + bpy.context.scene.unit_settings.scale_length = old_scale + + # We delete the baked action and set the original one back + for i in range(0, len(object_action_pairs)): + + object_action_pairs[i][0].animation_data.action = original_actions[i] + + object_action_pairs[i][1].user_clear() + bpy.data.actions.remove(object_action_pairs[i][1]) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From 50bff7fcc0b968c9113d3980e6b656b57d4c32f1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 17:26:45 +0100 Subject: [PATCH 072/327] bulk is actually written to database --- pype/plugins/global/publish/integrate_master_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 3df74f4e28..ea97f3d779 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -272,7 +272,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ) if bulk_writes: - pass + io._database[io.Session["AVALON_PROJECT"]].bulk_write(bulk_writes) def create_hardlink(self, src_path, dst_path): dst_path = self.path_root_check(dst_path) From 1c098196e69496aa8237b06ebcd36fe34d4db74b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 17:46:05 +0100 Subject: [PATCH 073/327] added few debug logs --- .../publish/integrate_master_version.py | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index ea97f3d779..390c86afce 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -25,6 +25,11 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ] def process(self, instance): + self.log.debug( + "Integrate of Master version for subset `{}` begins.".format( + instance.data.get("subset", str(instance)) + ) + ) published_repres = instance.data.get("published_representations") if not published_repres: self.log.debug( @@ -37,15 +42,21 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # TODO raise error if master not set? anatomy = instance.context.data["anatomy"] if "publish" not in anatomy.templates: - self.warning("Anatomy does not have set publish key!") + self.log.warning("Anatomy does not have set publish key!") return if "master" not in anatomy.templates["publish"]: - self.warning(( + self.log.warning(( "There is not set \"master\" template for project \"{}\"" ).format(project_name)) return + master_template = anatomy.templates["publish"]["master"] + + self.log.debug("`Master` template check was successful. `{}`".format( + master_template + )) + src_version_entity = None filtered_repre_ids = [] @@ -55,6 +66,11 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): src_version_entity = repre_info.get("version_entity") if repre["name"].lower() in self.ignored_representation_names: + self.log.debug( + "Filtering representation with name: `{}`".format( + repre["name"].lower() + ) + ) filtered_repre_ids.append(repre_id) for repre_id in filtered_repre_ids: @@ -67,12 +83,19 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): return if src_version_entity is None: + self.log.debug(( + "Published version entity was not sent in representation data." + " Querying entity from database." + )) src_version_entity = ( self.version_from_representations(published_repres) ) if not src_version_entity: - self.log.warning("Can't find origin version in database.") + self.log.warning(( + "Can't find origin version in database." + " Skipping Master version publish." + )) return old_version, old_repres = ( @@ -99,6 +122,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): bulk_writes = [] if old_version: + self.log.debug("Replacing old master version.") bulk_writes.append( ReplaceOne( {"_id": new_master_version["_id"]}, @@ -106,6 +130,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ) ) else: + self.log.debug("Creating first master version.") bulk_writes.append( InsertOne(new_master_version) ) @@ -282,11 +307,17 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): try: os.makedirs(dirname) + self.log.debug("Folder created: \"{}\"".format(dirname)) except OSError as exc: if exc.errno != errno.EEXIST: self.log.error("An unexpected error occurred.", exc_info=True) raise + self.log.debug("Folder already exists: \"{}\"".format(dirname)) + + self.log.debug("Copying file \"{}\" to \"{}\"".format( + src_path, dst_path + )) filelink.create(src_path, dst_path, filelink.HARDLINK) def path_root_check(self, path): From 03b252556de70a121a273b5acc0474b33c035327 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 18:19:12 +0100 Subject: [PATCH 074/327] keep only one master_template variable --- pype/plugins/global/publish/integrate_master_version.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 390c86afce..dc15ff2d8d 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -161,8 +161,6 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): self.delete_repre_files(old_repres) - master_template = anatomy.templates["publish"]["master"] - src_to_dst_file_paths = [] for repre_id, repre_info in published_repres.items(): @@ -193,7 +191,6 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): repre_context[key] = anatomy_data[key] - # TODO change repre data and context (new anatomy) # Prepare new repre repre = copy.deepcopy(repre_info["representation"]) repre["parent"] = new_master_version["_id"] @@ -232,7 +229,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): InsertOne(repre) ) - # TODO hardlink files + # Prepare paths of source and destination files if len(published_files) == 1: src_to_dst_file_paths.append( (published_files[0], template_filled) From 236da4f8849042b65d0192e7f0749638891b3870 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 18:21:54 +0100 Subject: [PATCH 075/327] fixed backup file handling --- .../publish/integrate_master_version.py | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index dc15ff2d8d..d98767cbfd 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -263,7 +263,9 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): (src_file, dst_file) ) + # Copy(hardlink) paths of source and destination files # TODO should we *only* create hardlinks? + # TODO less logs about drives for src_path, dst_path in src_to_dst_file_paths: self.create_hardlink(src_path, dst_path) @@ -443,17 +445,24 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): renamed_files = [] failed = False for file_path in files_to_delete: - # TODO too robust for testing - should be easier in future - _rename_path = file_path + ".BACKUP" - rename_path = None - max_index = 200 - cur_index = 1 - while True: - if max_index >= cur_index: - raise Exception(( + self.log.debug( + "Preparing file for deletion: `{}`".format(file_path) + ) + rename_path = file_path + ".BACKUP" + + max_index = 10 + cur_index = 0 + _rename_path = None + while os.path.exists(rename_path): + if _rename_path is None: + _rename_path = rename_path + + if cur_index >= max_index: + self.log.warning(( "Max while loop index reached! Can't make backup" " for previous master version." )) + failed = True break if not os.path.exists(_rename_path): @@ -462,21 +471,41 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): try: os.remove(_rename_path) + self.log.debug( + "Deleted old backup file: \"{}\"".format(_rename_path) + ) except Exception: + self.log.warning( + "Could not delete old backup file \"{}\".".format( + _rename_path + ), + exc_info=True + ) _rename_path = file_path + ".BACKUP{}".format( str(cur_index) ) cur_index += 1 + # Skip if any already failed + if failed: + break + try: args = (file_path, rename_path) os.rename(*args) renamed_files.append(args) except Exception: + self.log.warning( + "Could not rename file `{}` to `{}`".format( + file_path, rename_path + ), + exc_info=True + ) failed = True break if failed: + # Rename back old renamed files for dst_name, src_name in renamed_files: os.rename(src_name, dst_name) From 390acf4eb394496486b7127b1fa9e75d01fececc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 18:24:42 +0100 Subject: [PATCH 076/327] addde important TODO --- pype/plugins/global/publish/integrate_master_version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index d98767cbfd..be6602ac13 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -317,6 +317,8 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): self.log.debug("Copying file \"{}\" to \"{}\"".format( src_path, dst_path )) + # TODO check if file exists!!! + # - uncomplete publish may cause that file already exists filelink.create(src_path, dst_path, filelink.HARDLINK) def path_root_check(self, path): From ae1b102f4756545ee670972e059c2db96603d712 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 18:39:52 +0100 Subject: [PATCH 077/327] begin and ending logs have 3 symbol start --- .../publish/integrate_master_version.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index be6602ac13..de2cedc2d7 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -26,14 +26,14 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): def process(self, instance): self.log.debug( - "Integrate of Master version for subset `{}` begins.".format( + "--- Integration of Master version for subset `{}` begins.".format( instance.data.get("subset", str(instance)) ) ) published_repres = instance.data.get("published_representations") if not published_repres: self.log.debug( - "There are not published representations on the instance." + "*** There are not published representations on the instance." ) return @@ -42,12 +42,12 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # TODO raise error if master not set? anatomy = instance.context.data["anatomy"] if "publish" not in anatomy.templates: - self.log.warning("Anatomy does not have set publish key!") + self.log.warning("!!! Anatomy does not have set publish key!") return if "master" not in anatomy.templates["publish"]: self.log.warning(( - "There is not set \"master\" template for project \"{}\"" + "!!! There is not set \"master\" template for project \"{}\"" ).format(project_name)) return @@ -78,7 +78,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): if not published_repres: self.log.debug( - "All published representations were filtered by name." + "*** All published representations were filtered by name." ) return @@ -93,7 +93,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): if not src_version_entity: self.log.warning(( - "Can't find origin version in database." + "!!! Can't find origin version in database." " Skipping Master version publish." )) return @@ -241,7 +241,8 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): raise Exception(( "Integrity error. Files of published representation" " is combination of frame collections and single files." - )) + "Collections: `{}` Single files: `{}`" + ).format(str(collections), str(remainders))) src_col = collections[0] @@ -266,13 +267,12 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # Copy(hardlink) paths of source and destination files # TODO should we *only* create hardlinks? # TODO less logs about drives + # TODO should we keep files for deletion until this is successful? for src_path, dst_path in src_to_dst_file_paths: self.create_hardlink(src_path, dst_path) # Archive not replaced old representations for repre_name_low, repre in old_repres_to_delete.items(): - # TODO delete their files - # Replace archived representation (This is backup) # - should not happen to have both repre and archived repre if repre_name_low in archived_repres_by_name: @@ -298,6 +298,12 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): if bulk_writes: io._database[io.Session["AVALON_PROJECT"]].bulk_write(bulk_writes) + self.log.debug(( + "--- End of Master version integration for subset `{}`." + ).format( + instance.data.get("subset", str(instance)) + )) + def create_hardlink(self, src_path, dst_path): dst_path = self.path_root_check(dst_path) src_path = self.path_root_check(src_path) From e5108a6e37e66fa3ffcf78df7a4872b1a0f517cd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 18:40:40 +0100 Subject: [PATCH 078/327] reduced logs about drive remapping --- .../publish/integrate_master_version.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index de2cedc2d7..a32c94b43e 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -264,9 +264,10 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): (src_file, dst_file) ) + self.path_checks = [] + # Copy(hardlink) paths of source and destination files # TODO should we *only* create hardlinks? - # TODO less logs about drives # TODO should we keep files for deletion until this is successful? for src_path, dst_path in src_to_dst_file_paths: self.create_hardlink(src_path, dst_path) @@ -333,9 +334,13 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): drive, _path = os.path.splitdrive(normalized_path) if os.path.exists(drive + "/"): - self.log.debug( - "Drive \"{}\" exist. Nothing to change.".format(drive) - ) + key = "drive_check{}".format(drive) + if key not in self.path_checks: + self.log.debug( + "Drive \"{}\" exist. Nothing to change.".format(drive) + ) + self.path_checks.append(key) + return normalized_path path_env_key = "PYPE_STUDIO_PROJECTS_PATH" @@ -348,15 +353,18 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): missing_envs.append(mount_env_key) if missing_envs: - _add_s = "" - if len(missing_envs) > 1: - _add_s = "s" + key = "missing_envs" + if key not in self.path_checks: + self.path_checks.append(key) + _add_s = "" + if len(missing_envs) > 1: + _add_s = "s" - self.log.warning(( - "Can't replace MOUNT drive path to UNC path due to missing" - " environment variable{}: `{}`. This may cause issues during" - " publishing process." - ).format(_add_s, ", ".join(missing_envs))) + self.log.warning(( + "Can't replace MOUNT drive path to UNC path due to missing" + " environment variable{}: `{}`. This may cause issues" + " during publishing process." + ).format(_add_s, ", ".join(missing_envs))) return normalized_path From 58ca4399a1db3264b1af054a80c2d2a96a44c5ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 24 Feb 2020 18:45:24 +0100 Subject: [PATCH 079/327] removed unused log --- pype/plugins/global/publish/integrate_master_version.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index a32c94b43e..f767a312d6 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -10,20 +10,21 @@ from avalon import api, io, pipeline from avalon.vendor import filelink -log = logging.getLogger(__name__) - - class IntegrateMasterVersion(pyblish.api.InstancePlugin): label = "Integrate Master Version" # Must happen after IntegrateNew order = pyblish.api.IntegratorOrder + 0.1 + # Can specify representation names that will be ignored (lower case) ignored_representation_names = [] db_representation_context_keys = [ "project", "asset", "task", "subset", "representation", "family", "hierarchy", "task", "username" ] - + # TODO add family filtering + # QUESTION/TODO this process should happen on server if crashed due to + # permissions error on files (files were used or user didn't have perms) + # *but all other plugins must be sucessfully completed def process(self, instance): self.log.debug( "--- Integration of Master version for subset `{}` begins.".format( From 4e833a4f44153988fe90e342c15d72873def3b89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 25 Feb 2020 14:05:45 +0100 Subject: [PATCH 080/327] master version do not rename each file but whole pusblish folder, also is used master.path anatomy instead of publish.master --- .../publish/integrate_master_version.py | 551 +++++++++--------- 1 file changed, 283 insertions(+), 268 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index f767a312d6..42c93db7e9 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -1,8 +1,8 @@ import os import copy -import logging import clique import errno +import shutil from pymongo import InsertOne, ReplaceOne import pyblish.api @@ -25,6 +25,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # QUESTION/TODO this process should happen on server if crashed due to # permissions error on files (files were used or user didn't have perms) # *but all other plugins must be sucessfully completed + def process(self, instance): self.log.debug( "--- Integration of Master version for subset `{}` begins.".format( @@ -42,24 +43,25 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # TODO raise error if master not set? anatomy = instance.context.data["anatomy"] - if "publish" not in anatomy.templates: - self.log.warning("!!! Anatomy does not have set publish key!") + if "master" not in anatomy.templates: + self.log.warning("!!! Anatomy does not have set `master` key!") return - if "master" not in anatomy.templates["publish"]: + if "path" not in anatomy.templates["master"]: self.log.warning(( - "!!! There is not set \"master\" template for project \"{}\"" + "!!! There is not set `path` template in `master` anatomy" + " for project \"{}\"." ).format(project_name)) return - master_template = anatomy.templates["publish"]["master"] - + master_template = anatomy.templates["master"]["path"] self.log.debug("`Master` template check was successful. `{}`".format( master_template )) - src_version_entity = None + master_publish_dir = self.get_publish_dir(instance) + src_version_entity = None filtered_repre_ids = [] for repre_id, repre_info in published_repres.items(): repre = repre_info["representation"] @@ -99,6 +101,47 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): )) return + all_copied_files = [] + transfers = instance.data.get("transfers", list()) + for src, dst in transfers: + dst = os.path.normpath(dst) + if dst not in all_copied_files: + all_copied_files.append(dst) + + hardlinks = instance.data.get("hardlinks", list()) + for src, dst in hardlinks: + dst = os.path.normpath(dst) + if dst not in all_copied_files: + all_copied_files.append(dst) + + all_repre_file_paths = [] + for repre_info in published_repres: + published_files = repre_info.get("published_files") or [] + for file_path in published_files: + file_path = os.path.normpath(file_path) + if file_path not in all_repre_file_paths: + all_repre_file_paths.append(file_path) + + # TODO this is not best practice of getting resources for publish + # WARNING due to this we must remove all files from master publish dir + instance_publish_dir = os.path.normpath( + instance.data["publishDir"] + ) + other_file_paths_mapping = [] + for file_path in all_copied_files: + # Check if it is from publishDir + if not file_path.startswith(instance_publish_dir): + continue + + if file_path in all_repre_file_paths: + continue + + dst_filepath = file_path.replace( + instance_publish_dir, master_publish_dir + ) + other_file_paths_mapping.append((file_path, dst_filepath)) + + # Current version old_version, old_repres = ( self.current_master_ents(src_version_entity) ) @@ -120,6 +163,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): "schema": "pype:master_version-1.0" } + # Don't make changes in database until everything is O.K. bulk_writes = [] if old_version: @@ -160,145 +204,212 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): repre_name_low = repre["name"].lower() archived_repres_by_name[repre_name_low] = repre - self.delete_repre_files(old_repres) + if os.path.exists(master_publish_dir): + backup_master_publish_dir = master_publish_dir + ".BACKUP" + max_idx = 10 + idx = 0 + _backup_master_publish_dir = backup_master_publish_dir + while os.path.exists(_backup_master_publish_dir): + self.log.debug(( + "Backup folder already exists." + " Trying to remove \"{}\"" + ).format(_backup_master_publish_dir)) - src_to_dst_file_paths = [] - for repre_id, repre_info in published_repres.items(): + try: + shutil.rmtree(_backup_master_publish_dir) + backup_master_publish_dir = _backup_master_publish_dir + break + except Exception: + self.log.info(( + "Could not remove previous backup folder." + " Trying to add index to folder name" + )) - # Skip if new repre does not have published repre files - published_files = repre_info["published_files"] - if len(published_files) == 0: - continue + _backup_master_publish_dir = ( + backup_master_publish_dir + str(idx) + ) + if not os.path.exists(_backup_master_publish_dir): + backup_master_publish_dir = _backup_master_publish_dir + break - # Prepare anatomy data - anatomy_data = repre_info["anatomy_data"] - anatomy_data.pop("version", None) + if idx > max_idx: + raise AssertionError(( + "Backup folders are fully occupied to max index \"{}\"" + ).format(max_idx)) + break - # Get filled path to repre context - anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled["publish"]["master"] + idx += 1 - repre_data = { - "path": str(template_filled), - "template": master_template - } - repre_context = template_filled.used_values - for key in self.db_representation_context_keys: - if ( - key in repre_context or - key not in anatomy_data - ): + self.log.debug("Backup folder path is \"{}\"".format( + backup_master_publish_dir + )) + try: + os.rename(master_publish_dir, backup_master_publish_dir) + except PermissionError: + raise AssertionError(( + "Could not create master version because it is not" + " possible to replace current master files." + )) + try: + src_to_dst_file_paths = [] + for repre_id, repre_info in published_repres.items(): + + # Skip if new repre does not have published repre files + published_files = repre_info["published_files"] + if len(published_files) == 0: continue - repre_context[key] = anatomy_data[key] + # Prepare anatomy data + anatomy_data = repre_info["anatomy_data"] + anatomy_data.pop("version", None) - # Prepare new repre - repre = copy.deepcopy(repre_info["representation"]) - repre["parent"] = new_master_version["_id"] - repre["context"] = repre_context - repre["data"] = repre_data + # Get filled path to repre context + anatomy_filled = anatomy.format(anatomy_data) + template_filled = anatomy_filled["publish"]["master"] - repre_name_low = repre["name"].lower() + repre_data = { + "path": str(template_filled), + "template": master_template + } + repre_context = template_filled.used_values + for key in self.db_representation_context_keys: + if ( + key in repre_context or + key not in anatomy_data + ): + continue - # Replace current representation - if repre_name_low in old_repres_to_replace: - old_repre = old_repres_to_replace.pop(repre_name_low) - repre["_id"] = old_repre["_id"] - bulk_writes.append( - ReplaceOne( - {"_id": old_repre["_id"]}, - repre + repre_context[key] = anatomy_data[key] + + # Prepare new repre + repre = copy.deepcopy(repre_info["representation"]) + repre["parent"] = new_master_version["_id"] + repre["context"] = repre_context + repre["data"] = repre_data + + repre_name_low = repre["name"].lower() + + # Replace current representation + if repre_name_low in old_repres_to_replace: + old_repre = old_repres_to_replace.pop(repre_name_low) + repre["_id"] = old_repre["_id"] + bulk_writes.append( + ReplaceOne( + {"_id": old_repre["_id"]}, + repre + ) ) - ) - # Unarchive representation - elif repre_name_low in archived_repres_by_name: - archived_repre = archived_repres_by_name.pop(repre_name_low) - old_id = archived_repre["old_id"] - repre["_id"] = old_id - bulk_writes.append( - ReplaceOne( - {"old_id": old_id}, - repre + # Unarchive representation + elif repre_name_low in archived_repres_by_name: + archived_repre = archived_repres_by_name.pop( + repre_name_low ) - ) - - # Create representation - else: - repre["_id"] = io.ObjectId() - bulk_writes.append( - InsertOne(repre) - ) - - # Prepare paths of source and destination files - if len(published_files) == 1: - src_to_dst_file_paths.append( - (published_files[0], template_filled) - ) - continue - - collections, remainders = clique.assemble(published_files) - if remainders or not collections or len(collections) > 1: - raise Exception(( - "Integrity error. Files of published representation" - " is combination of frame collections and single files." - "Collections: `{}` Single files: `{}`" - ).format(str(collections), str(remainders))) - - src_col = collections[0] - - # Get head and tail for collection - frame_splitter = "_-_FRAME_SPLIT_-_" - anatomy_data["frame"] = frame_splitter - _anatomy_filled = anatomy.format(anatomy_data) - _template_filled = _anatomy_filled["publish"]["master"] - head, tail = _template_filled.split(frame_splitter) - padding = ( - anatomy.templates["render"]["padding"] - ) - - dst_col = clique.Collection(head=head, padding=padding, tail=tail) - dst_col.indexes.clear() - dst_col.indexes.update(src_col.indexes) - for src_file, dst_file in zip(src_col, dst_col): - src_to_dst_file_paths.append( - (src_file, dst_file) - ) - - self.path_checks = [] - - # Copy(hardlink) paths of source and destination files - # TODO should we *only* create hardlinks? - # TODO should we keep files for deletion until this is successful? - for src_path, dst_path in src_to_dst_file_paths: - self.create_hardlink(src_path, dst_path) - - # Archive not replaced old representations - for repre_name_low, repre in old_repres_to_delete.items(): - # Replace archived representation (This is backup) - # - should not happen to have both repre and archived repre - if repre_name_low in archived_repres_by_name: - archived_repre = archived_repres_by_name.pop(repre_name_low) - repre["old_id"] = repre["_id"] - repre["_id"] = archived_repre["_id"] - repre["type"] = archived_repre["type"] - bulk_writes.append( - ReplaceOne( - {"_id": archived_repre["_id"]}, - repre + old_id = archived_repre["old_id"] + repre["_id"] = old_id + bulk_writes.append( + ReplaceOne( + {"old_id": old_id}, + repre + ) ) + + # Create representation + else: + repre["_id"] = io.ObjectId() + bulk_writes.append( + InsertOne(repre) + ) + + # Prepare paths of source and destination files + if len(published_files) == 1: + src_to_dst_file_paths.append( + (published_files[0], template_filled) + ) + continue + + collections, remainders = clique.assemble(published_files) + if remainders or not collections or len(collections) > 1: + raise Exception(( + "Integrity error. Files of published representation " + "is combination of frame collections and single files." + "Collections: `{}` Single files: `{}`" + ).format(str(collections), str(remainders))) + + src_col = collections[0] + + # Get head and tail for collection + frame_splitter = "_-_FRAME_SPLIT_-_" + anatomy_data["frame"] = frame_splitter + _anatomy_filled = anatomy.format(anatomy_data) + _template_filled = _anatomy_filled["master"]["path"] + head, tail = _template_filled.split(frame_splitter) + padding = ( + anatomy.templates["render"]["padding"] ) - else: - repre["old_id"] = repre["_id"] - repre["_id"] = io.ObjectId() - repre["type"] = "archived_representation" - bulk_writes.append( - InsertOne(repre) + dst_col = clique.Collection( + head=head, padding=padding, tail=tail + ) + dst_col.indexes.clear() + dst_col.indexes.update(src_col.indexes) + for src_file, dst_file in zip(src_col, dst_col): + src_to_dst_file_paths.append( + (src_file, dst_file) + ) + + self.path_checks = [] + + # Copy(hardlink) paths of source and destination files + # TODO should we *only* create hardlinks? + # TODO should we keep files for deletion until this is successful? + for src_path, dst_path in src_to_dst_file_paths: + self.create_hardlink(src_path, dst_path) + + for src_path, dst_path in other_file_paths_mapping: + self.create_hardlink(src_path, dst_path) + + # Archive not replaced old representations + for repre_name_low, repre in old_repres_to_delete.items(): + # Replace archived representation (This is backup) + # - should not happen to have both repre and archived repre + if repre_name_low in archived_repres_by_name: + archived_repre = archived_repres_by_name.pop( + repre_name_low + ) + repre["old_id"] = repre["_id"] + repre["_id"] = archived_repre["_id"] + repre["type"] = archived_repre["type"] + bulk_writes.append( + ReplaceOne( + {"_id": archived_repre["_id"]}, + repre + ) + ) + + else: + repre["old_id"] = repre["_id"] + repre["_id"] = io.ObjectId() + repre["type"] = "archived_representation" + bulk_writes.append( + InsertOne(repre) + ) + + if bulk_writes: + io._database[io.Session["AVALON_PROJECT"]].bulk_write( + bulk_writes ) - if bulk_writes: - io._database[io.Session["AVALON_PROJECT"]].bulk_write(bulk_writes) + # Remove backuped previous master + shutil.rmtree(backup_master_publish_dir) + + except Exception: + os.rename(backup_master_publish_dir, master_publish_dir) + self.log.error(( + "!!! Creating of Master version failed." + " Previous master version maybe lost some data!" + )) + raise self.log.debug(( "--- End of Master version integration for subset `{}`." @@ -306,7 +417,49 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): instance.data.get("subset", str(instance)) )) + def get_all_files_from_path(self, path): + files = [] + for (dir_path, dir_names, file_names) in os.walk(path): + for file_name in file_names: + _path = os.path.join(dir_path, file_name) + files.append(_path) + return files + + def get_publish_dir(self, instance): + anatomy = instance.context.data["anatomy"] + template_data = copy.deepcopy(instance.data["anatomyData"]) + + if "folder" in anatomy.templates["master"]: + anatomy_filled = anatomy.format(template_data) + publish_folder = anatomy_filled["master"]["folder"] + else: + # This is for cases of Deprecated anatomy without `folder` + # TODO remove when all clients have solved this issue + template_data.update({ + "frame": "FRAME_TEMP", + "representation": "TEMP" + }) + anatomy_filled = anatomy.format(template_data) + # solve deprecated situation when `folder` key is not underneath + # `publish` anatomy + project_name = api.Session["AVALON_PROJECT"] + self.log.warning(( + "Deprecation warning: Anatomy does not have set `folder`" + " key underneath `publish` (in global of for project `{}`)." + ).format(project_name)) + + file_path = anatomy_filled["master"]["path"] + # Directory + publish_folder = os.path.dirname(file_path) + + publish_folder = os.path.normpath(publish_folder) + + self.log.debug("Master publish dir: \"{}\"".format(publish_folder)) + + return publish_folder + def create_hardlink(self, src_path, dst_path): + # TODO check drives if are the same to check if cas hardlink dst_path = self.path_root_check(dst_path) src_path = self.path_root_check(src_path) @@ -314,7 +467,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): try: os.makedirs(dirname) - self.log.debug("Folder created: \"{}\"".format(dirname)) + self.log.debug("Folder(s) created: \"{}\"".format(dirname)) except OSError as exc: if exc.errno != errno.EEXIST: self.log.error("An unexpected error occurred.", exc_info=True) @@ -325,8 +478,6 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): self.log.debug("Copying file \"{}\" to \"{}\"".format( src_path, dst_path )) - # TODO check if file exists!!! - # - uncomplete publish may cause that file already exists filelink.create(src_path, dst_path, filelink.HARDLINK) def path_root_check(self, path): @@ -398,142 +549,6 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): return os.path.normpath(path) - def delete_repre_files(self, repres): - if not repres: - return - - frame_splitter = "_-_FRAME_-_" - files_to_delete = [] - for repre in repres: - is_sequence = False - if "frame" in repre["context"]: - repre["context"]["frame"] = frame_splitter - is_sequence = True - - template = repre["data"]["template"] - context = repre["context"] - context["root"] = api.registered_root() - path = pipeline.format_template_with_optional_keys( - context, template - ) - path = os.path.normpath(path) - if not is_sequence: - if os.path.exists(path): - files_to_delete.append(path) - continue - - dirpath = os.path.dirname(path) - file_start = None - file_end = None - file_items = path.split(frame_splitter) - if len(file_items) == 0: - continue - elif len(file_items) == 1: - if path.startswith(frame_splitter): - file_end = file_items[0] - else: - file_start = file_items[1] - - elif len(file_items) == 2: - file_start, file_end = file_items - - else: - raise ValueError(( - "Representation template has `frame` key " - "more than once inside." - )) - - for file_name in os.listdir(dirpath): - check_name = str(file_name) - if file_start and not check_name.startswith(file_start): - continue - check_name.replace(file_start, "") - - if file_end and not check_name.endswith(file_end): - continue - check_name.replace(file_end, "") - - # File does not have frame - if not check_name: - continue - - files_to_delete.append(os.path.join(dirpath, file_name)) - - renamed_files = [] - failed = False - for file_path in files_to_delete: - self.log.debug( - "Preparing file for deletion: `{}`".format(file_path) - ) - rename_path = file_path + ".BACKUP" - - max_index = 10 - cur_index = 0 - _rename_path = None - while os.path.exists(rename_path): - if _rename_path is None: - _rename_path = rename_path - - if cur_index >= max_index: - self.log.warning(( - "Max while loop index reached! Can't make backup" - " for previous master version." - )) - failed = True - break - - if not os.path.exists(_rename_path): - rename_path = _rename_path - break - - try: - os.remove(_rename_path) - self.log.debug( - "Deleted old backup file: \"{}\"".format(_rename_path) - ) - except Exception: - self.log.warning( - "Could not delete old backup file \"{}\".".format( - _rename_path - ), - exc_info=True - ) - _rename_path = file_path + ".BACKUP{}".format( - str(cur_index) - ) - cur_index += 1 - - # Skip if any already failed - if failed: - break - - try: - args = (file_path, rename_path) - os.rename(*args) - renamed_files.append(args) - except Exception: - self.log.warning( - "Could not rename file `{}` to `{}`".format( - file_path, rename_path - ), - exc_info=True - ) - failed = True - break - - if failed: - # Rename back old renamed files - for dst_name, src_name in renamed_files: - os.rename(src_name, dst_name) - - raise AssertionError(( - "Could not create master version because it is not possible" - " to replace current master files." - )) - - for _, renamed_path in renamed_files: - os.remove(renamed_path) - def version_from_representations(self, repres): for repre in repres: version = io.find_one({"_id": repre["parent"]}) From 685edf184383dd7e6cc75f1b29568622522aa001 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 25 Feb 2020 14:12:00 +0100 Subject: [PATCH 081/327] few minor fixes --- pype/plugins/global/publish/integrate_master_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 42c93db7e9..f2769a436e 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -115,7 +115,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): all_copied_files.append(dst) all_repre_file_paths = [] - for repre_info in published_repres: + for repre_info in published_repres.values(): published_files = repre_info.get("published_files") or [] for file_path in published_files: file_path = os.path.normpath(file_path) @@ -265,7 +265,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # Get filled path to repre context anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled["publish"]["master"] + template_filled = anatomy_filled["master"]["path"] repre_data = { "path": str(template_filled), From 4ced37437b3d875b24fbfe7fb8c613d45ab7d6f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 25 Feb 2020 14:26:23 +0100 Subject: [PATCH 082/327] create_hardlink changed to copy_file - can handle if paths are cross drives --- .../publish/integrate_master_version.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index f2769a436e..2a23abfbec 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -364,10 +364,10 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # TODO should we *only* create hardlinks? # TODO should we keep files for deletion until this is successful? for src_path, dst_path in src_to_dst_file_paths: - self.create_hardlink(src_path, dst_path) + self.copy_file(src_path, dst_path) for src_path, dst_path in other_file_paths_mapping: - self.create_hardlink(src_path, dst_path) + self.copy_file(src_path, dst_path) # Archive not replaced old representations for repre_name_low, repre in old_repres_to_delete.items(): @@ -412,7 +412,8 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): raise self.log.debug(( - "--- End of Master version integration for subset `{}`." + "--- Master version integration for subset `{}`" + " seems to be successful." ).format( instance.data.get("subset", str(instance)) )) @@ -458,7 +459,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): return publish_folder - def create_hardlink(self, src_path, dst_path): + def copy_file(self, src_path, dst_path): # TODO check drives if are the same to check if cas hardlink dst_path = self.path_root_check(dst_path) src_path = self.path_root_check(src_path) @@ -478,7 +479,19 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): self.log.debug("Copying file \"{}\" to \"{}\"".format( src_path, dst_path )) - filelink.create(src_path, dst_path, filelink.HARDLINK) + + # First try hardlink and copy if paths are cross drive + try: + filelink.create(src_path, dst_path, filelink.HARDLINK) + # Return when successful + return + + except OSError as exc: + # re-raise exception if different than cross drive path + if exc.errno != errno.EXDEV: + raise + + shutil.copy(src_path, dst_path) def path_root_check(self, path): normalized_path = os.path.normpath(path) From 666041c9c94aa94bcb0f43460b5af957799b39a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 25 Feb 2020 14:54:59 +0100 Subject: [PATCH 083/327] added schema validation and fixed master version schema --- .../plugins/global/publish/integrate_master_version.py | 7 +++++-- schema/master_version-1.0.json | 10 ++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 2a23abfbec..715d99c1c8 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -6,7 +6,7 @@ import shutil from pymongo import InsertOne, ReplaceOne import pyblish.api -from avalon import api, io, pipeline +from avalon import api, io, schema from avalon.vendor import filelink @@ -162,6 +162,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): "type": "master_version", "schema": "pype:master_version-1.0" } + schema.validate(new_master_version) # Don't make changes in database until everything is O.K. bulk_writes = [] @@ -286,9 +287,11 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): repre["parent"] = new_master_version["_id"] repre["context"] = repre_context repre["data"] = repre_data + repre.pop("_id", None) + + schema.validate(repre) repre_name_low = repre["name"].lower() - # Replace current representation if repre_name_low in old_repres_to_replace: old_repre = old_repres_to_replace.pop(repre_name_low) diff --git a/schema/master_version-1.0.json b/schema/master_version-1.0.json index 173a076537..991594648b 100644 --- a/schema/master_version-1.0.json +++ b/schema/master_version-1.0.json @@ -17,14 +17,13 @@ "properties": { "_id": { "description": "Document's id (database will create it's if not entered)", - "type": "ObjectId", - "example": "592c33475f8c1b064c4d1696" + "example": "ObjectId(592c33475f8c1b064c4d1696)" }, "schema": { "description": "The schema associated with this document", "type": "string", - "enum": ["avalon-core:master_version-3.0", "pype:master_version-3.0"], - "example": "pype:master_version-3.0" + "enum": ["avalon-core:master_version-1.0", "pype:master_version-1.0"], + "example": "pype:master_version-1.0" }, "type": { "description": "The type of document", @@ -34,8 +33,7 @@ }, "parent": { "description": "Unique identifier to parent document", - "type": "ObjectId", - "example": "592c33475f8c1b064c4d1696" + "example": "ObjectId(592c33475f8c1b064c4d1697)" } } } From 5a56df384d0cdb4fd3c19bf64841269a26b8c025 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 2 Mar 2020 12:25:15 +0100 Subject: [PATCH 084/327] set cpp project mode on demand --- pype/hooks/unreal/unreal_prelaunch.py | 12 +- pype/unreal/lib.py | 173 ++++++++++++++++++-------- 2 files changed, 127 insertions(+), 58 deletions(-) diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index cb3b6e8e64..efb5d9157b 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -42,7 +42,7 @@ class UnrealPrelaunch(PypeHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # :scream: + # 😱 if not project_name[:1].isalpha(): self.log.warning(f"Project name doesn't start with alphabet " f"character ({project_name}). Appending 'P'") @@ -67,17 +67,17 @@ class UnrealPrelaunch(PypeHook): os.makedirs(project_path, exist_ok=True) project_file = os.path.join(project_path, f"{project_name}.uproject") + engine_path = detected[engine_version] if not os.path.isfile(project_file): self.log.info((f"{self.signature} creating unreal " f"project [ {project_name} ]")) if env.get("AVALON_UNREAL_PLUGIN"): os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501 unreal_lib.create_unreal_project(project_name, - engine_version, project_path) - - self.log.info((f"{self.signature} preparing unreal project ... ")) - unreal_lib.prepare_project(project_file, detected[engine_version]) + engine_version, + project_path, + engine_path=engine_path) env["PYPE_UNREAL_PROJECT_FILE"] = project_file - env["AVALON_CURRENT_UNREAL_ENGINE"] = detected[engine_version] + env["AVALON_CURRENT_UNREAL_ENGINE"] = engine_path return True diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 6130b38764..be6314b09b 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -4,6 +4,7 @@ import platform import json from distutils import dir_util import subprocess +from pypeapp import config def get_engine_versions(): @@ -63,7 +64,7 @@ def _win_get_engine_versions(): return _parse_launcher_locations(install_json_path) -def _darwin_get_engine_version(): +def _darwin_get_engine_version() -> dict: """ It works the same as on Windows, just JSON file location is different. """ @@ -79,7 +80,16 @@ def _darwin_get_engine_version(): return _parse_launcher_locations(install_json_path) -def _parse_launcher_locations(install_json_path): +def _parse_launcher_locations(install_json_path: str) -> dict: + """ + This will parse locations from json file. + + :param install_json_path: path to `LauncherInstalled.dat` + :type install_json_path: str + :returns: returns dict with unreal engine versions as keys and + paths to those engine installations as value. + :rtype: dict + """ engine_locations = {} if os.path.isfile(install_json_path): with open(install_json_path, "r") as ilf: @@ -99,7 +109,11 @@ def _parse_launcher_locations(install_json_path): return engine_locations -def create_unreal_project(project_name, ue_version, dir): +def create_unreal_project(project_name: str, + ue_version: str, + pr_dir: str, + engine_path: str, + dev_mode: bool = False) -> None: """ This will create `.uproject` file at specified location. As there is no way I know to create project via command line, this is easiest option. @@ -107,12 +121,30 @@ def create_unreal_project(project_name, ue_version, dir): `AVALON_UNREAL_PLUGIN` environment variable we assume this is location of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. + + :param project_name: project name + :type project_name: str + :param ue_version: unreal engine version (like 4.23) + :type ue_version: str + :param pr_dir: path to directory where project will be created + :type pr_dir: str + :param engine_path: Path to Unreal Engine installation + :type engine_path: str + :param dev_mode: Flag to trigger C++ style Unreal project needing + Visual Studio and other tools to compile plugins from + sources. This will trigger automatically if `Binaries` + directory is not found in plugin folders as this indicates + this is only source distribution of the plugin. Dev mode + is also set by preset file `unreal/project_setup.json` in + **PYPE_CONFIG**. + :type dev_mode: bool + :returns: None """ - import git + preset = config.get_presets()["unreal"]["project_setup"] if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugins_path = os.path.join(dir, "Plugins") + plugins_path = os.path.join(pr_dir, "Plugins") avalon_plugin_path = os.path.join(plugins_path, "Avalon") if not os.path.isdir(avalon_plugin_path): os.makedirs(avalon_plugin_path, exist_ok=True) @@ -120,68 +152,105 @@ def create_unreal_project(project_name, ue_version, dir): dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), avalon_plugin_path) - # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to - # support offline installation. - # Otherwise clone UnrealEnginePython to Plugins directory - # https://github.com/20tab/UnrealEnginePython.git - uep_path = os.path.join(plugins_path, "UnrealEnginePython") - if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): - - os.makedirs(uep_path, exist_ok=True) - dir_util._path_created = {} - dir_util.copy_tree(os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path) - else: - git.Repo.clone_from("https://github.com/20tab/UnrealEnginePython.git", - uep_path) - - # now we need to fix python path in: - # `UnrealEnginePython.Build.cs` - # to point to our python - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="r") as f: - build_file = f.read() - - fix = build_file.replace( - 'private string pythonHome = "";', - 'private string pythonHome = "{}";'.format( - sys.base_prefix.replace("\\", "/"))) - - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="w") as f: - f.write(fix) + if (not os.path.isdir(os.path.join(avalon_plugin_path, "Binaries")) + or not os.path.join(avalon_plugin_path, "Intermediate")): + dev_mode = True + # data for project file data = { "FileVersion": 3, "EngineAssociation": ue_version, "Category": "", "Description": "", - "Modules": [ - { + "Plugins": [ + {"Name": "PythonScriptPlugin", "Enabled": True}, + {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "Avalon", "Enabled": True} + ] + } + + if preset["install_unreal_python_engine"]: + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. + # Otherwise clone UnrealEnginePython to Plugins directory + # https://github.com/20tab/UnrealEnginePython.git + uep_path = os.path.join(plugins_path, "UnrealEnginePython") + if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + + os.makedirs(uep_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree( + os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + uep_path) + else: + # WARNING: this will trigger dev_mode, because we need to compile + # this plugin. + dev_mode = True + import git + git.Repo.clone_from( + "https://github.com/20tab/UnrealEnginePython.git", + uep_path) + + data["Plugins"].append( + {"Name": "UnrealEnginePython", "Enabled": True}) + + if (not os.path.isdir(os.path.join(uep_path, "Binaries")) + or not os.path.join(uep_path, "Intermediate")): + dev_mode = True + + if dev_mode or preset["dev_mode"]: + # this will add project module and necessary source file to make it + # C++ project and to (hopefully) make Unreal Editor to compile all + # sources at start + + data["Modules"] = [{ "Name": project_name, "Type": "Runtime", "LoadingPhase": "Default", "AdditionalDependencies": ["Engine"], - } - ], - "Plugins": [ - {"Name": "PythonScriptPlugin", "Enabled": True}, - {"Name": "EditorScriptingUtilities", "Enabled": True}, - {"Name": "Avalon", "Enabled": True}, - {"Name": "UnrealEnginePython", "Enabled": True} - ], - } + }] - project_file = os.path.join(dir, "{}.uproject".format(project_name)) + if preset["install_unreal_python_engine"]: + # now we need to fix python path in: + # `UnrealEnginePython.Build.cs` + # to point to our python + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="r") as f: + build_file = f.read() + + fix = build_file.replace( + 'private string pythonHome = "";', + 'private string pythonHome = "{}";'.format( + sys.base_prefix.replace("\\", "/"))) + + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="w") as f: + f.write(fix) + + # write project file + project_file = os.path.join(pr_dir, "{}.uproject".format(project_name)) with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) + # ensure we have PySide installed in engine + # TODO: make it work for other platforms 🍎 🐧 + if platform.system().lower() == "windows": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Win64", + "python.exe") -def prepare_project(project_file: str, engine_path: str): + subprocess.run([python_path, "-m", + "pip", "install", "pyside"]) + + if dev_mode or preset["dev_mode"]: + _prepare_cpp_project(pr_dir, engine_path) + + +def _prepare_cpp_project(project_file: str, engine_path: str) -> None: """ This function will add source files needed for project to be rebuild along with the avalon integration plugin. From cf43bbb18e1b6bf453241132a283a847d06e3ef1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:34:12 +0100 Subject: [PATCH 085/327] moved imports to top of lib --- pype/lib.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ad3a863854..39d8533be8 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1,14 +1,19 @@ import os import re +import uuid +import json +import getpass +import collections import logging import itertools import contextlib import subprocess import inspect +import platform -from avalon import io +from avalon import io, pipeline import avalon.api -import avalon +from pypeapp import config log = logging.getLogger(__name__) @@ -489,7 +494,6 @@ def filter_pyblish_plugins(plugins): `discover()` method. :type plugins: Dict """ - from pypeapp import config from pyblish import api host = api.current_host() @@ -546,7 +550,6 @@ def get_subsets(asset_name, Returns: dict: subsets with version and representaions in keys """ - from avalon import io # query asset from db asset_io = io.find_one({"type": "asset", "name": asset_name}) @@ -620,7 +623,6 @@ class CustomNone: def __init__(self): """Create uuid as identifier for custom None.""" - import uuid self.identifier = str(uuid.uuid4()) def __bool__(self): From 116a16eb4c0ab04bd3f29c6d39889455e08ddfaf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:34:48 +0100 Subject: [PATCH 086/327] get_workfile_build_presets method implemented to load workfile variants per host and current task --- pype/lib.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 39d8533be8..06f3540177 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -643,3 +643,30 @@ class CustomNone: def __repr__(self): """Representation of custom None.""" return "".format(str(self.identifier)) + + +def get_workfile_build_presets(task_name): + host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] + presets = config.get_presets(io.Session["AVALON_PROJECT"]) + # Get presets for host + workfile_presets = presets["plugins"].get(host_name, {}).get( + "workfile_build" + ) + if not workfile_presets: + return + + task_name_low = task_name.lower() + per_task_preset = None + for variant in workfile_presets: + variant_tasks = variant.get("tasks") + if not variant_tasks: + continue + + variant_tasks_low = [task.lower() for task in variant_tasks] + if task_name_low not in variant_tasks_low: + continue + + per_task_preset = variant + break + + return per_task_preset From 99ea83939a8707babe8ffe803a1bc78e870f1c17 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:36:24 +0100 Subject: [PATCH 087/327] collect_last_version_repres implemented to get all representations of latest versions for all subsets of entered assets --- pype/lib.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 06f3540177..5cdcf82d4d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -670,3 +670,98 @@ def get_workfile_build_presets(task_name): break return per_task_preset + + +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + :param asset_entities: Asset entities for which want to find data + :type asset_entities: list + :return: collected entities + :rtype: dict + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(io.find({ + "type": "subset", + "parent": {"$in": asset_entity_by_ids.keys()} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(io.find({ + "type": "version", + "parent": {"$in": subset_entity_by_ids.keys()} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = io.find({ + "type": "representation", + "parent": {"$in": last_versions_by_id.keys()} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output From a415e2255af906e940124eb8715fb253531690a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:36:56 +0100 Subject: [PATCH 088/327] added get_link_assets, not yet implemented since we dont have the logic --- pype/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 5cdcf82d4d..3e92349d10 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -672,6 +672,12 @@ def get_workfile_build_presets(task_name): return per_task_preset +def get_link_assets(asset_entity): + """Return linked assets for `asset_entity`.""" + # TODO implement + return [test_asset] + + def collect_last_version_repres(asset_entities): """Collect subsets, versions and representations for asset_entities. From 57f6f9b87d8280aedff63f0c3155fc897181ff27 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:37:47 +0100 Subject: [PATCH 089/327] implemented load_containers_by_asset_data which loads all containers for specific asset by entered workfile variants --- pype/lib.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 3e92349d10..30c222ac57 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -672,6 +672,215 @@ def get_workfile_build_presets(task_name): return per_task_preset +def load_containers_by_asset_data( + asset_entity_data, workfile_presets, loaders_by_name +): + if not asset_entity_data or not workfile_presets or not loaders_by_name: + return + + asset_entity = asset_entity_data["asset_entity"] + # Filter workfile presets by available loaders + valid_variants = [] + for variant in workfile_presets: + variant_loaders = variant.get("loaders") + if not variant_loaders: + log.warning(( + "Workfile variant has missing loaders configuration: {0}" + ).format(json.dumps(variant, indent=4))) + continue + + found = False + for loader_name in variant_loaders: + if loader_name in loaders_by_name: + valid_variants.append(variant) + found = True + break + + if not found: + log.warning( + "Loaders in Workfile variant are not available: {0}".format( + json.dumps(variant, indent=4) + ) + ) + + if not valid_variants: + log.warning("There are not valid Workfile variants. Skipping process.") + return + + log.debug("Valid Workfile variants: {}".format(valid_variants)) + + subsets = [] + version_by_subset_id = {} + repres_by_version_id = {} + for subset_id, in_data in asset_entity_data["subsets"].items(): + subsets.append(in_data["subset_entity"]) + version_data = in_data["version"] + version_entity = version_data["version_entity"] + version_by_subset_id[subset_id] = version_entity + repres_by_version_id[version_entity["_id"]] = version_data["repres"] + + if not subsets: + log.warning("There are not subsets for asset {0}".format( + asset_entity["name"] + )) + return + + subsets_by_family = collections.defaultdict(list) + for subset in subsets: + family = subset["data"].get("family") + if not family: + families = subset["data"].get("families") + if not families: + continue + family = families[0] + + subsets_by_family[family].append(subset) + + valid_subsets_by_id = {} + variants_per_subset_id = {} + for family, subsets in subsets_by_family.items(): + family_low = family.lower() + for variant in valid_variants: + # Family filtering + variant_families = variant.get("families") or [] + if not variant_families: + continue + + variant_families_low = [fam.lower() for fam in variant_families] + if family_low not in variant_families_low: + continue + + # Regex filtering (optional) + variant_regexes = variant.get("subset_filters") + for subset in subsets: + if variant_regexes: + valid = False + for pattern in variant_regexes: + if re.match(pattern, subset["name"]): + valid = True + break + + if not valid: + continue + + subset_id = subset["_id"] + valid_subsets_by_id[subset_id] = subset + variants_per_subset_id[subset_id] = variant + + # break variants loop if got here + break + + if not valid_subsets_by_id: + log.warning("There are not valid subsets.") + return + + log.debug("Valid subsets: {}".format(valid_subsets_by_id.values())) + + valid_repres_by_subset_id = collections.defaultdict(list) + for subset_id, subset_entity in valid_subsets_by_id.items(): + variant = variants_per_subset_id[subset_id] + variant_repre_names = variant.get("repre_names") + if not variant_repre_names: + continue + + # Lower names + variant_repre_names = [name.lower() for name in variant_repre_names] + + version_entity = version_by_subset_id[subset_id] + version_id = version_entity["_id"] + repres = repres_by_version_id[version_id] + for repre in repres: + repre_name_low = repre["name"].lower() + if repre_name_low in variant_repre_names: + valid_repres_by_subset_id[subset_id].append(repre) + + # DEBUG message + msg = "Valid representations for Asset: `{}`".format(asset_entity["name"]) + for subset_id, repres in valid_repres_by_subset_id.items(): + subset = valid_subsets_by_id[subset_id] + msg += "\n# Subset Name/ID: `{}`/{}".format(subset["name"], subset_id) + for repre in repres: + msg += "\n## Repre name: `{}`".format(repre["name"]) + + log.debug(msg) + + loaded_containers = { + "asset_entity": asset_entity, + "containers": [] + } + + for subset_id, repres in valid_repres_by_subset_id.items(): + subset = valid_subsets_by_id[subset_id] + subset_name = subset["name"] + + variant = variants_per_subset_id[subset_id] + + variant_loader_names = variant["loaders"] + variant_loader_count = len(variant_loader_names) + + variant_repre_names = variant["repre_names"] + variant_repre_count = len(variant_repre_names) + + is_loaded = False + for repre_name_idx, variant_repre_name in enumerate( + variant_repre_names + ): + found_repre = None + for repre in repres: + repre_name = repre["name"] + if repre_name == variant_repre_name: + found_repre = repre + break + + if not found_repre: + continue + + for loader_idx, loader_name in enumerate(variant_loader_names): + if is_loaded: + break + + loader = loaders_by_name.get(loader_name) + if not loader: + continue + try: + container = avalon.api.load( + loader, + found_repre["_id"], + name=subset_name + ) + loaded_containers["containers"].append(container) + is_loaded = True + + except Exception as exc: + if exc == pipeline.IncompatibleLoaderError: + log.info(( + "Loader `{}` is not compatible with" + " representation `{}`" + ).format(loader_name, repre["name"])) + + else: + log.error( + "Unexpected error happened during loading", + exc_info=True + ) + + msg = "Loading failed." + if loader_idx < (variant_loader_count - 1): + msg += " Trying next loader." + elif repre_name_idx < (variant_repre_count - 1): + msg += ( + " Loading of subset `{}` was not successful." + ).format(subset_name) + else: + msg += " Trying next representation." + log.info(msg) + + if is_loaded: + break + + return loaded_containers + + def get_link_assets(asset_entity): """Return linked assets for `asset_entity`.""" # TODO implement From 808510d856fb5ba4562630b9673dcff630b4b88f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:39:12 +0100 Subject: [PATCH 090/327] implemented load_containers_for_workfile which loads and trigger loading by current context --- pype/lib.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 30c222ac57..f37da5096a 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -980,3 +980,138 @@ def collect_last_version_repres(asset_entities): ) return output + + +def load_containers_for_workfile(): + """Load containers for (first) workfile. + + Loads latest versions of current and linked assets to workfile by logic + stored in Workfile variants from presets. Variants are set by host, + filtered by current task name and used by families. + + Each family can specify representation names and loaders for + representations and first available and successful loaded representation is + returned as container. + + At the end you'll get list of loaded containers per each asset. + + loaded_containers [{ + "asset_entity": , + "containers": [, , ...] + }, { + "asset_entity": , + "containers": [, ...] + }, { + ... + }] + """ + io.install() + + # Get current asset name and entity + current_asset_name = io.Session["AVALON_ASSET"] + current_asset_entity = io.find_one({ + "type": "asset", + "name": current_asset_name + }) + + # Skip if asset was not found + if not current_asset_entity: + print("Asset entity with name `{}` was not found".format( + current_asset_name + )) + return + + # Prepare available loaders + loaders_by_name = {} + for loader in avalon.api.discover(avalon.api.Loader): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError("Duplicated loader name {0}!".format(loader_name)) + loaders_by_name[loader_name] = loader + + # Skip if there are any loaders + if not loaders_by_name: + print("There are not registered loaders.") + return + + # Get current task name + current_task_name = os.environ["AVALON_TASK"] + current_task_name_low = current_task_name.lower() + # Load workfile presets for task + workfile_presets = get_workfile_build_presets(current_task_name_low) + + # Skip if there are any presets for task + if not workfile_presets: + log.warning( + "For current task `{}` is not set any loading preset.".format( + current_task_name + ) + ) + return + + # Get presets for loading current asset + current_context = workfile_presets.get("current_context") + # Get presets for loading linked assets + link_context = workfile_presets.get("linked_assets") + # Skip if both are missing + if not current_context and not link_context: + log.warning("Current task `{}` has empty loading preset.".format( + current_task_name + )) + return + + elif not current_context: + log.warning(( + "Current task `{}` don't have set loading preset for it's context." + ).format(current_task_name)) + + elif not link_context: + log.warning(( + "Current task `{}` don't have set " + "loading preset for it's linked assets." + ).format(current_task_name)) + + # Prepare assets to process by workfile presets + assets = [] + current_asset_id = None + if current_context: + # Add current asset entity if preset has current context set + assets.append(current_asset_entity) + current_asset_id = current_asset_entity["_id"] + + if link_context: + # Find and append linked assets if preset has set linked mapping + link_assets = get_link_assets(current_asset_entity) + if link_assets: + assets.extend(link_assets) + + # Skip if there are any assets + # - this may happend if only linked mapping is set and there are not links + if not assets: + log.warning("Asset does not have linked assets. Nothing to process.") + return + + # Prepare entities from database for assets + prepared_entities = collect_last_version_repres(assets) + + # Load containers by prepared entities and presets + loaded_containers = [] + # - Current asset containers + if current_asset_id and current_asset_id in prepared_entities: + current_context_data = prepared_entities.pop(current_asset_id) + loaded_data = load_containers_by_asset_data( + current_context_data, current_context, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # - Linked assets container + for linked_asset_data in prepared_entities.values(): + loaded_data = load_containers_by_asset_data( + linked_asset_data, link_context, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # Return list of loaded containers + return loaded_containers From 149a65f1e7f5c371c3f51bfc06932d05b6bcd60a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:39:36 +0100 Subject: [PATCH 091/327] implemented methods for creating first version workfile --- pype/lib.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index f37da5096a..4e7d88947d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1115,3 +1115,126 @@ def load_containers_for_workfile(): # Return list of loaded containers return loaded_containers + + +def get_last_workfile_path(root, template, file_ext): + template = re.sub("<.*?>", ".*?", template) + template = re.sub("{version.*}", "([0-9]+)", template) + template = re.sub("{comment.*?}", ".+?", template) + # template = pipeline._format_work_template(template) + template = "^" + template + "$" + + all_file_names = [] + if os.path.exists(root): + all_file_names = os.listdir(root) + + filtered_file_names = [ + file_name for file_name in all_file_names + if os.path.splitext(file_name)[1] == file_ext + ] + + kwargs = {} + if platform.system() == "Windows": + kwargs["flags"] = re.IGNORECASE + + version = None + last_file_name = None + for file_name in sorted(filtered_file_names): + match = re.match(template, file_name, **kwargs) + if not match: + continue + + file_version = int(match.group(1)) + if file_version >= version: + last_file_name = file_name + version = file_version + 1 + + last_file_path = None + if last_file_name: + last_file_path = os.path.join(root, last_file_name) + + return last_file_path + + +def create_first_workfile(file_ext=None): + """Builds first workfile and load containers for it. + + :param file_ext: Work file extension may be specified otherwise first + extension in host's registered extensions is used. + :type file_ext: str + :return: Workfile path and loaded containers by Asset entity + :rtype: tuple + """ + # Get host + host = avalon.api.registered_host() + + # Workfile extension + if file_ext is None: + if not host.file_extensions(): + raise AssertionError( + "Host doesn't have set file extensions. Can't create workfile." + ) + file_ext = host.file_extensions()[0] + + workfile_root = host.work_root(io.Session) + + # make sure extension has dot + if not file_ext.startswith("."): + file_ext = ".{}".format(file_ext) + + # Create new workfile + project_doc = io.find_one({"type": "project"}) + asset_name = io.Session["AVALON_ASSET"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + if not asset_doc: + raise AssertionError( + "Asset with name `{}` was not found.".format(asset_name) + ) + + root = avalon.api.registered_root() + template = project_doc["config"]["template"]["workfile"] + file_path = get_last_workfile_path(root, template, file_ext) + # TODO what should do if already exists? + # 1.) create new + # 2.) override + # 3.) raise exception + if file_path is not None: + log.warning("There already exist workfile `{}`.".format(file_path)) + return file_path + + hierarchy = "" + parents = asset_doc["data"].get("parents") + if parents: + hierarchy = "/".join(parents) + + # Use same data as Workfiles tool + template_data = { + "root": root, + "project": { + "name": project_doc["name"], + "code": project_doc["data"].get("code") + }, + "asset": asset_name, + "task": io.Session["AVALON_TASK"], + "hierarchy": hierarchy, + "version": 1, + "user": getpass.getuser(), + "ext": file_ext + } + + # Use same template as in Workfiles Tool + template_filled = pipeline.format_template_with_optional_keys( + template_data, template + ) + + # make sure filled template does not have more dots due to extension + while ".." in template_filled: + template_filled = template_filled.replace("..", ".") + + workfile_path = os.path.join(workfile_root, template_filled) + host.save_file(workfile_path) + + return workfile_path From d38df643a837ecc47c86362421ae85ee7c635e4f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:39:57 +0100 Subject: [PATCH 092/327] added build first workfile (probably for testing) --- pype/lib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 4e7d88947d..0698e2bbba 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1238,3 +1238,12 @@ def create_first_workfile(file_ext=None): host.save_file(workfile_path) return workfile_path + + +def build_first_workfile(file_ext=None): + # DEBUG this should probably be host specific + # Create workfile + workfile_path = create_first_workfile(file_ext) + # Load containers + loaded_containers = load_containers_for_workfile() + return (workfile_path, loaded_containers) From 4428ccc04a286e9a4d3028ac90340ffe662c0cbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:40:18 +0100 Subject: [PATCH 093/327] added build first workfile to maya menu --- pype/maya/menu.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/pype/maya/menu.py b/pype/maya/menu.py index 806944c117..efe338b4af 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -3,7 +3,9 @@ import os import logging from avalon.vendor.Qt import QtWidgets, QtCore, QtGui - +from avalon.maya import pipeline +import avalon.api +from ..lib import build_first_workfile import maya.cmds as cmds self = sys.modules[__name__] @@ -21,8 +23,15 @@ def _get_menu(): return menu - def deferred(): + def add_build_workfiles_item(): + # Add build first workfile + cmds.menuItem(divider=True, parent=pipeline._menu) + cmds.menuItem( + "Build First Workfile", + parent=pipeline._menu, + command=lambda *args: build_first_workfile() + ) log.info("Attempting to install scripts menu..") @@ -30,8 +39,11 @@ def deferred(): import scriptsmenu.launchformaya as launchformaya import scriptsmenu.scriptsmenu as scriptsmenu except ImportError: - log.warning("Skipping studio.menu install, because " - "'scriptsmenu' module seems unavailable.") + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + add_build_workfiles_item() return # load configuration of custom menu @@ -39,15 +51,16 @@ def deferred(): config = scriptsmenu.load_configuration(config_path) # run the launcher for Maya menu - studio_menu = launchformaya.main(title=self._menu.title(), - objectName=self._menu) + studio_menu = launchformaya.main( + title=self._menu.title(), + objectName=self._menu + ) # apply configuration studio_menu.build_from_configuration(studio_menu, config) def uninstall(): - menu = _get_menu() if menu: log.info("Attempting to uninstall..") @@ -60,9 +73,8 @@ def uninstall(): def install(): - if cmds.about(batch=True): - print("Skipping pype.menu initialization in batch mode..") + log.info("Skipping pype.menu initialization in batch mode..") return uninstall() From c11092252f2b455fb4a7922d6661a46b08f9c74d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:45:55 +0100 Subject: [PATCH 094/327] fixed not existing variable --- pype/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 0698e2bbba..d39902f5cf 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -884,7 +884,7 @@ def load_containers_by_asset_data( def get_link_assets(asset_entity): """Return linked assets for `asset_entity`.""" # TODO implement - return [test_asset] + return [] def collect_last_version_repres(asset_entities): From 7d0d0f4270c8dd0f0e62ad0a63033e3d4ea0dc75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:48:07 +0100 Subject: [PATCH 095/327] renamed key `subset_filters` to `subset_name_filters` --- pype/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index d39902f5cf..0264653f0f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -751,7 +751,7 @@ def load_containers_by_asset_data( continue # Regex filtering (optional) - variant_regexes = variant.get("subset_filters") + variant_regexes = variant.get("subset_name_filters") for subset in subsets: if variant_regexes: valid = False From ec411a4b5dfd4b1bef1553236f7c0820de9581b2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 4 Mar 2020 17:00:00 +0000 Subject: [PATCH 096/327] Added 'action' family and small adjustments --- pype/plugins/blender/create/create_action.py | 38 +++ .../blender/create/create_animation.py | 2 + pype/plugins/blender/load/load_action.py | 295 ++++++++++++++++++ pype/plugins/blender/load/load_animation.py | 9 +- pype/plugins/blender/load/load_model.py | 4 + .../plugins/blender/publish/collect_action.py | 53 ++++ .../blender/publish/collect_animation.py | 2 +- pype/plugins/blender/publish/extract_blend.py | 2 +- pype/plugins/global/publish/integrate_new.py | 3 +- 9 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 pype/plugins/blender/create/create_action.py create mode 100644 pype/plugins/blender/load/load_action.py create mode 100644 pype/plugins/blender/publish/collect_action.py diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py new file mode 100644 index 0000000000..88ecebdfff --- /dev/null +++ b/pype/plugins/blender/create/create_action.py @@ -0,0 +1,38 @@ +"""Create an animation asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateAction(Creator): + """Action output for character rigs""" + + name = "actionMain" + label = "Action" + family = "action" + icon = "male" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + if obj.animation_data is not None and obj.animation_data.action is not None: + + empty_obj = bpy.data.objects.new( name = name, object_data = None ) + empty_obj.animation_data_create() + empty_obj.animation_data.action = obj.animation_data.action + empty_obj.animation_data.action.name = name + collection.objects.link(empty_obj) + + return collection diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index cfe569f918..14a50ba5ea 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -1,3 +1,5 @@ +"""Create an animation asset.""" + import bpy from avalon import api diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py new file mode 100644 index 0000000000..6094f712ae --- /dev/null +++ b/pype/plugins/blender/load/load_action.py @@ -0,0 +1,295 @@ +"""Load an action in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_action") + + +class BlendAnimationLoader(pype.blender.AssetLoader): + """Load action from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["action"] + representations = ["blend"] + + label = "Link Action" + icon = "code-fork" + color = "orange" + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in animation_container.objects: + + obj = obj.make_local() + + # obj.data.make_local() + + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + strips = [] + + for obj in collection_metadata["objects"]: + + for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + + if armature_obj.animation_data is not None: + + for track in armature_obj.animation_data.nla_tracks: + + for strip in track.strips: + + if strip.action == obj.animation_data.action: + + strips.append(strip) + + bpy.data.actions.remove(obj.animation_data.action) + bpy.data.objects.remove(obj) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in animation_container.objects: + + obj = obj.make_local() + + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + + for strip in strips: + + strip.action = obj.animation_data.action + strip.action_frame_end = obj.animation_data.action.frame_range[1] + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + + objects_list.append(obj) + + animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + + if armature_obj.animation_data is not None: + + for track in armature_obj.animation_data.nla_tracks: + + for strip in track.strips: + + if strip.action == obj.animation_data.action: + + track.strips.remove(strip) + + bpy.data.actions.remove(obj.animation_data.action) + bpy.data.objects.remove(obj) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 58a0e94665..c6d18fb1a9 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -10,15 +10,12 @@ import bpy import pype.blender from avalon import api -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") +logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") class BlendAnimationLoader(pype.blender.AssetLoader): """Load animations from a .blend file. - Because they come from a .blend file we can simply link the collection that - contains the model. There is no further need to 'containerise' it. - Warning: Loading the same asset more then once is not properly supported at the moment. @@ -94,6 +91,10 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj.data.make_local() + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 40d6c3434c..8ba8c5cfc8 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -87,6 +87,10 @@ class BlendModelLoader(pype.blender.AssetLoader): obj.data.make_local() + for material_slot in obj.material_slots: + + material_slot.material.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py new file mode 100644 index 0000000000..0b5e468920 --- /dev/null +++ b/pype/plugins/blender/publish/collect_action.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectAnimation(pyblish.api.ContextPlugin): + """Collect the data of an action.""" + + hosts = ["blender"] + label = "Collect Action" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_action_collections() -> Generator: + """Return all 'animation' collections. + + Check if the family is 'action' and if it doesn't have the + representation set. If the representation is set, it is a loaded action + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'action' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the actions from the current Blender scene.""" + collections = self.get_action_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 9bc0b02227..109ae98e6f 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -20,7 +20,7 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded rig + representation set. If the representation is set, it is a loaded animation and we don't want to publish it. """ for collection in bpy.data.collections: diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 7e11e9ef8d..032f85897d 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["animation", "model", "rig"] + families = ["animation", "model", "rig", "action"] optional = True def process(self, instance): diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 813417bdfc..86ada2f111 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -78,7 +78,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "matchmove", "image" "source", - "assembly" + "assembly", + "action" ] exclude_families = ["clip"] db_representation_context_keys = [ From de972a2fa45c22cc6b5337e342028372261028ac Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 5 Mar 2020 10:27:57 +0000 Subject: [PATCH 097/327] Fixed a naming issue --- pype/plugins/blender/publish/collect_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index 0b5e468920..9a54045cea 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -8,7 +8,7 @@ import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY -class CollectAnimation(pyblish.api.ContextPlugin): +class CollectAction(pyblish.api.ContextPlugin): """Collect the data of an action.""" hosts = ["blender"] From 5bf25ffd3ee322e16c508bda88322faafa198e04 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Mar 2020 12:39:25 +0000 Subject: [PATCH 098/327] Bug fixing and code optimization --- pype/plugins/blender/create/create_rig.py | 63 ++++++- pype/plugins/blender/load/load_action.py | 6 +- pype/plugins/blender/load/load_animation.py | 161 ++++++++---------- pype/plugins/blender/load/load_model.py | 146 +++++++---------- pype/plugins/blender/load/load_rig.py | 172 ++++++++------------ 5 files changed, 262 insertions(+), 286 deletions(-) diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 5d83fafdd3..f630c63966 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -14,6 +14,23 @@ class CreateRig(Creator): family = "rig" icon = "wheelchair" + # @staticmethod + # def _find_layer_collection(self, layer_collection, collection): + + # found = None + + # if (layer_collection.collection == collection): + + # return layer_collection + + # for layer in layer_collection.children: + + # found = self._find_layer_collection(layer, collection) + + # if found: + + # return found + def process(self): import pype.blender @@ -25,8 +42,52 @@ class CreateRig(Creator): self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. + # Blender crashes if trying to link an object that is already linked. + # This links automatically the children meshes if they were not + # selected, and doesn't link them twice if they, insted, + # were manually selected by the user. + objects_to_link = set() + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): - collection.objects.link(obj) + + objects_to_link.add( obj ) + + if obj.type == 'ARMATURE': + + for subobj in obj.children: + + objects_to_link.add( subobj ) + + # Create a new collection and link the widgets that + # the rig uses. + # custom_shapes = set() + + # for posebone in obj.pose.bones: + + # if posebone.custom_shape is not None: + + # custom_shapes.add( posebone.custom_shape ) + + # if len( custom_shapes ) > 0: + + # widgets_collection = bpy.data.collections.new(name="Widgets") + + # collection.children.link(widgets_collection) + + # for custom_shape in custom_shapes: + + # widgets_collection.objects.link( custom_shape ) + + # layer_collection = self._find_layer_collection(bpy.context.view_layer.layer_collection, widgets_collection) + + # layer_collection.exclude = True + + for obj in objects_to_link: + + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 6094f712ae..747bcd47f5 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -13,7 +13,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_action") -class BlendAnimationLoader(pype.blender.AssetLoader): +class BlendActionLoader(pype.blender.AssetLoader): """Load action from a .blend file. Warning: @@ -47,7 +47,6 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) - relative = bpy.context.preferences.filepaths.use_relative_paths container = bpy.data.collections.new(lib_container) container.name = container_name @@ -65,6 +64,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container + relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -85,8 +85,6 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj = obj.make_local() - # obj.data.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: obj.animation_data.action.make_local() diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index c6d18fb1a9..0610517b67 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -28,43 +28,22 @@ class BlendAnimationLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name): - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - lib_container = pype.blender.plugin.asset_name(asset, subset) - container_name = pype.blender.plugin.asset_name( - asset, subset, namespace - ) relative = bpy.context.preferences.filepaths.use_relative_paths - - container = bpy.data.collections.new(lib_container) - container.name = container_name - avalon.blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -77,8 +56,9 @@ class BlendAnimationLoader(pype.blender.AssetLoader): animation_container = scene.collection.children[lib_container].make_local() meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + armatures = [obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + + # Should check if there is only an armature? objects_list = [] @@ -106,11 +86,51 @@ class BlendAnimationLoader(pype.blender.AssetLoader): animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process(self, libpath, lib_container, container_name) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - nodes = list(container.objects) nodes.append(container) self[:] = nodes @@ -177,59 +197,16 @@ class BlendAnimationLoader(pype.blender.AssetLoader): logger.info("Library already loaded, not updating...") return - # Get the armature of the rig - armatures = [obj for obj in collection_metadata["objects"] - if obj.type == 'ARMATURE'] - assert(len(armatures) == 1) - - for obj in collection_metadata["objects"]: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - + objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - animation_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in animation_container.objects if obj.type == 'ARMATURE'] - objects_list = [] - + # Get the armature of the rig + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: + self._remove(self, objects, lib_container) - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -266,14 +243,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) + self._remove(self, objects, lib_container) + bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 8ba8c5cfc8..10904a1f7b 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -31,43 +31,19 @@ class BlendModelLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name): - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - lib_container = pype.blender.plugin.asset_name(asset, subset) - container_name = pype.blender.plugin.asset_name( - asset, subset, namespace - ) relative = bpy.context.preferences.filepaths.use_relative_paths - - container = bpy.data.collections.new(lib_container) - container.name = container_name - avalon.blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -102,13 +78,53 @@ class BlendModelLoader(pype.blender.AssetLoader): model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + + collection = bpy.data.collections.new(lib_container) + collection.name = container_name + avalon.blender.pipeline.containerise_existing( + collection, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process(self, libpath, lib_container, container_name) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - - nodes = list(container.objects) - nodes.append(container) + nodes = list(collection.objects) + nodes.append(collection) self[:] = nodes return nodes @@ -154,8 +170,10 @@ class BlendModelLoader(pype.blender.AssetLoader): collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -171,54 +189,15 @@ class BlendModelLoader(pype.blender.AssetLoader): logger.info("Library already loaded, not updating...") return - for obj in collection_metadata["objects"]: + self._remove(self, objects, lib_container) - bpy.data.meshes.remove(obj.data) - - lib_container = collection_metadata["lib_container"] - - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - model_container = scene.collection.children[lib_container].make_local() - - objects_list = [] - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in model_container.objects: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) - bpy.ops.object.select_all(action='DESELECT') - def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -246,11 +225,8 @@ class BlendModelLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: + self._remove(self, objects, lib_container) - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index c19717cd82..dcb70da6d8 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -30,6 +30,68 @@ class BlendRigLoader(pype.blender.AssetLoader): label = "Link Rig" icon = "code-fork" color = "orange" + + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name, action): + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] + + objects_list = [] + + assert(len(armatures) == 1) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + if obj.type == 'ARMATURE' and action is not None: + + obj.animation_data.action = action + + objects_list.append(obj) + + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + + bpy.ops.object.select_all(action='DESELECT') + + return objects_list def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -50,7 +112,6 @@ class BlendRigLoader(pype.blender.AssetLoader): container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) - relative = bpy.context.preferences.filepaths.use_relative_paths container = bpy.data.collections.new(lib_container) container.name = container_name @@ -68,48 +129,11 @@ class BlendRigLoader(pype.blender.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - rig_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - - objects_list = [] - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) - - objects_list.append(obj) - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - nodes = list(container.objects) nodes.append(container) self[:] = nodes @@ -159,8 +183,10 @@ class BlendRigLoader(pype.blender.AssetLoader): collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -177,64 +203,14 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [obj for obj in collection_metadata["objects"] - if obj.type == 'ARMATURE'] + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) action = armatures[0].animation_data.action - for obj in collection_metadata["objects"]: + self._remove(self, objects, lib_container) - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - lib_container = collection_metadata["lib_container"] - - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - rig_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - objects_list = [] - - assert(len(armatures) == 1) - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - if obj.type == 'ARMATURE' and action is not None: - - obj.animation_data.action = action - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -271,14 +247,8 @@ class BlendRigLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) + self._remove(self, objects, lib_container) + bpy.data.collections.remove(collection) return True From 9a8655be1da5a8939a34cf66f71a036b13141fb2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 14 Mar 2020 00:41:53 +0100 Subject: [PATCH 099/327] publishing unreal static mesh from maya --- pype/hooks/unreal/unreal_prelaunch.py | 2 +- .../global/publish/collect_scene_version.py | 3 + pype/plugins/global/publish/integrate_new.py | 3 +- .../maya/create/create_unreal_staticmesh.py | 11 ++ .../maya/publish/collect_unreal_staticmesh.py | 33 ++++ pype/plugins/maya/publish/extract_fbx.py | 5 +- .../validate_unreal_mesh_triangulated.py | 33 ++++ .../validate_unreal_staticmesh_naming.py | 120 ++++++++++++++ .../maya/publish/validate_unreal_up_axis.py | 25 +++ pype/plugins/unreal/create/create_fbx.py | 14 ++ .../plugins/unreal/load/load_staticmeshfbx.py | 53 ++++++ .../unreal/publish/collect_instances.py | 152 ++++++++++++++++++ pype/unreal/__init__.py | 45 ++++++ pype/unreal/plugin.py | 9 ++ 14 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 pype/plugins/maya/create/create_unreal_staticmesh.py create mode 100644 pype/plugins/maya/publish/collect_unreal_staticmesh.py create mode 100644 pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py create mode 100644 pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py create mode 100644 pype/plugins/maya/publish/validate_unreal_up_axis.py create mode 100644 pype/plugins/unreal/create/create_fbx.py create mode 100644 pype/plugins/unreal/load/load_staticmeshfbx.py create mode 100644 pype/plugins/unreal/publish/collect_instances.py create mode 100644 pype/unreal/plugin.py diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index efb5d9157b..5b6b8e08e0 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -36,7 +36,7 @@ class UnrealPrelaunch(PypeHook): # Unreal is sensitive about project names longer then 20 chars if len(project_name) > 20: self.log.warning((f"Project name exceed 20 characters " - f"[ {project_name} ]!")) + f"({project_name})!")) # Unreal doesn't accept non alphabet characters at the start # of the project name. This is because project name is then used diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 02e913199b..314a64f550 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -16,6 +16,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if "standalonepublisher" in context.data.get("host", []): return + if "unreal" in context.data.get("host", []): + return + filename = os.path.basename(context.data.get('currentFile')) if '' in filename: diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 1d061af173..8935127e9e 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -80,7 +80,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "matchmove", "image" "source", - "assembly" + "assembly", + "fbx" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/pype/plugins/maya/create/create_unreal_staticmesh.py b/pype/plugins/maya/create/create_unreal_staticmesh.py new file mode 100644 index 0000000000..5a74cb22d5 --- /dev/null +++ b/pype/plugins/maya/create/create_unreal_staticmesh.py @@ -0,0 +1,11 @@ +import avalon.maya + + +class CreateUnrealStaticMesh(avalon.maya.Creator): + name = "staticMeshMain" + label = "Unreal - Static Mesh" + family = "unrealStaticMesh" + icon = "cube" + + def __init__(self, *args, **kwargs): + super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs) diff --git a/pype/plugins/maya/publish/collect_unreal_staticmesh.py b/pype/plugins/maya/publish/collect_unreal_staticmesh.py new file mode 100644 index 0000000000..5ab9643f4b --- /dev/null +++ b/pype/plugins/maya/publish/collect_unreal_staticmesh.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from maya import cmds +import pyblish.api + + +class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): + """Collect unreal static mesh + + Ensures always only a single frame is extracted (current frame). This + also sets correct FBX options for later extraction. + + Note: + This is a workaround so that the `pype.model` family can use the + same pointcache extractor implementation as animation and pointcaches. + This always enforces the "current" frame to be published. + + """ + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Model Data" + families = ["unrealStaticMesh"] + + def process(self, instance): + # add fbx family to trigger fbx extractor + instance.data["families"].append("fbx") + # set fbx overrides on instance + instance.data["smoothingGroups"] = True + instance.data["smoothMesh"] = True + instance.data["triangulate"] = True + + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame diff --git a/pype/plugins/maya/publish/extract_fbx.py b/pype/plugins/maya/publish/extract_fbx.py index 01b58241c2..6a75bfce0e 100644 --- a/pype/plugins/maya/publish/extract_fbx.py +++ b/pype/plugins/maya/publish/extract_fbx.py @@ -212,12 +212,11 @@ class ExtractFBX(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'mov', - 'ext': 'mov', + 'name': 'fbx', + 'ext': 'fbx', 'files': filename, "stagingDir": stagingDir, } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py b/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py new file mode 100644 index 0000000000..77f7144c4e --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api + + +class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): + """Validate if mesh is made of triangles for Unreal Engine""" + + order = pype.api.ValidateMeshOder + hosts = ["maya"] + families = ["unrealStaticMesh"] + category = "geometry" + label = "Mesh is Triangulated" + actions = [pype.maya.action.SelectInvalidAction] + + @classmethod + def get_invalid(cls, instance): + invalid = [] + meshes = cmds.ls(instance, type="mesh", long=True) + for mesh in meshes: + faces = cmds.polyEvaluate(mesh, f=True) + tris = cmds.polyEvaluate(mesh, t=True) + if faces != tris: + invalid.append(mesh) + + return invalid + + def process(self, instance): + invalid = self.get_invalid(instance) + assert len(invalid) == 0, ( + "Found meshes without triangles") diff --git a/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py b/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py new file mode 100644 index 0000000000..b62a855da9 --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api +import pype.maya.action +import re + + +class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin): + """Validate name of Unreal Static Mesh + + Unreals naming convention states that staticMesh sould start with `SM` + prefix - SM_[Name]_## (Eg. SM_sube_01). This plugin also validates other + types of meshes - collision meshes: + + UBX_[RenderMeshName]_##: + Boxes are created with the Box objects type in + Max or with the Cube polygonal primitive in Maya. + You cannot move the vertices around or deform it + in any way to make it something other than a + rectangular prism, or else it will not work. + + UCP_[RenderMeshName]_##: + Capsules are created with the Capsule object type. + The capsule does not need to have many segments + (8 is a good number) at all because it is + converted into a true capsule for collision. Like + boxes, you should not move the individual + vertices around. + + USP_[RenderMeshName]_##: + Spheres are created with the Sphere object type. + The sphere does not need to have many segments + (8 is a good number) at all because it is + converted into a true sphere for collision. Like + boxes, you should not move the individual + vertices around. + + UCX_[RenderMeshName]_##: + Convex objects can be any completely closed + convex 3D shape. For example, a box can also be + a convex object + + This validator also checks if collision mesh [RenderMeshName] matches one + of SM_[RenderMeshName]. + + """ + optional = True + order = pype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["unrealStaticMesh"] + label = "Unreal StaticMesh Name" + actions = [pype.maya.action.SelectInvalidAction] + regex_mesh = r"SM_(?P.*)_(\d{2})" + regex_collision = r"((UBX)|(UCP)|(USP)|(UCX))_(?P.*)_(\d{2})" + + @classmethod + def get_invalid(cls, instance): + + # find out if supplied transform is group or not + def is_group(groupName): + try: + children = cmds.listRelatives(groupName, children=True) + for child in children: + if not cmds.ls(child, transforms=True): + return False + return True + except Exception: + return False + + invalid = [] + content_instance = instance.data.get("setMembers", None) + if not content_instance: + cls.log.error("Instance has no nodes!") + return True + pass + descendants = cmds.listRelatives(content_instance, + allDescendents=True, + fullPath=True) or [] + + descendants = cmds.ls(descendants, noIntermediate=True, long=True) + trns = cmds.ls(descendants, long=False, type=('transform')) + + # filter out groups + filter = [node for node in trns if not is_group(node)] + + # compile regex for testing names + sm_r = re.compile(cls.regex_mesh) + cl_r = re.compile(cls.regex_collision) + + sm_names = [] + col_names = [] + for obj in filter: + sm_m = sm_r.match(obj) + if sm_m is None: + # test if it matches collision mesh + cl_r = sm_r.match(obj) + if cl_r is None: + cls.log.error("invalid mesh name on: {}".format(obj)) + invalid.append(obj) + else: + col_names.append((cl_r.group("renderName"), obj)) + else: + sm_names.append(sm_m.group("renderName")) + + for c_mesh in col_names: + if c_mesh[0] not in sm_names: + cls.log.error(("collision name {} doesn't match any " + "static mesh names.").format(obj)) + invalid.append(c_mesh[1]) + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + + if invalid: + raise RuntimeError("Model naming is invalid. See log.") diff --git a/pype/plugins/maya/publish/validate_unreal_up_axis.py b/pype/plugins/maya/publish/validate_unreal_up_axis.py new file mode 100644 index 0000000000..6641edb4a5 --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_up_axis.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api + + +class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): + """Validate if Z is set as up axis in Maya""" + + optional = True + order = pype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["unrealStaticMesh"] + label = "Unreal Up-Axis check" + actions = [pype.api.RepairAction] + + def process(self, context): + assert cmds.upAxis(q=True, axis=True) == "z", ( + "Invalid axis set as up axis" + ) + + @classmethod + def repair(cls, instance): + cmds.upAxis(axis="z", rotateView=True) diff --git a/pype/plugins/unreal/create/create_fbx.py b/pype/plugins/unreal/create/create_fbx.py new file mode 100644 index 0000000000..0d5b0bf316 --- /dev/null +++ b/pype/plugins/unreal/create/create_fbx.py @@ -0,0 +1,14 @@ +from pype.unreal.plugin import Creator + + +class CreateFbx(Creator): + """Static FBX geometry""" + + name = "modelMain" + label = "Model" + family = "model" + icon = "cube" + asset_types = ["StaticMesh"] + + def __init__(self, *args, **kwargs): + super(CreateFbx, self).__init__(*args, **kwargs) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py new file mode 100644 index 0000000000..056c81d54d --- /dev/null +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -0,0 +1,53 @@ +from avalon import api +from avalon import unreal as avalon_unreal +import unreal +import time + + +class StaticMeshFBXLoader(api.Loader): + """Load Unreal StaticMesh from FBX""" + + families = ["unrealStaticMesh"] + label = "Import FBX Static Mesh" + representations = ["fbx"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + + tools = unreal.AssetToolsHelpers().get_asset_tools() + temp_dir, temp_name = tools.create_unique_asset_name( + "/Game/{}".format(name), "_TMP" + ) + + # asset_path = "/Game/{}".format(namespace) + unreal.EditorAssetLibrary.make_directory(temp_dir) + + task = unreal.AssetImportTask() + + task.filename = self.fname + task.destination_path = temp_dir + task.destination_name = name + task.replace_existing = False + task.automated = True + task.save = True + + # set import options here + task.options = unreal.FbxImportUI() + task.options.import_animations = False + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + imported_assets = unreal.EditorAssetLibrary.list_assets( + temp_dir, recursive=True, include_folder=True + ) + new_dir = avalon_unreal.containerise( + name, namespace, imported_assets, context, self.__class__.__name__) + + asset_content = unreal.EditorAssetLibrary.list_assets( + new_dir, recursive=True, include_folder=True + ) + + unreal.EditorAssetLibrary.delete_directory(temp_dir) + + return asset_content diff --git a/pype/plugins/unreal/publish/collect_instances.py b/pype/plugins/unreal/publish/collect_instances.py new file mode 100644 index 0000000000..fa604f79d3 --- /dev/null +++ b/pype/plugins/unreal/publish/collect_instances.py @@ -0,0 +1,152 @@ +import unreal + +import pyblish.api + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by objectSet and pre-defined attribute + + This collector takes into account assets that are associated with + an objectSet and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance" + + Limitations: + - Does not take into account nodes connected to those + within an objectSet. Extractors are assumed to export + with history preserved, but this limits what they will + be able to achieve and the amount of data available + to validators. An additional collector could also + append this input data into the instance, as we do + for `pype.rig` with collect_history. + + """ + + label = "Collect Instances" + order = pyblish.api.CollectorOrder + hosts = ["unreal"] + + def process(self, context): + + objectset = cmds.ls("*.id", long=True, type="objectSet", + recursive=True, objectsOnly=True) + + context.data['objectsets'] = objectset + for objset in objectset: + + if not cmds.attributeQuery("id", node=objset, exists=True): + continue + + id_attr = "{}.id".format(objset) + if cmds.getAttr(id_attr) != "pyblish.avalon.instance": + continue + + # The developer is responsible for specifying + # the family of each instance. + has_family = cmds.attributeQuery("family", + node=objset, + exists=True) + assert has_family, "\"%s\" was missing a family" % objset + + members = cmds.sets(objset, query=True) + if members is None: + self.log.warning("Skipped empty instance: \"%s\" " % objset) + continue + + self.log.info("Creating instance for {}".format(objset)) + + data = dict() + + # Apply each user defined attribute as data + for attr in cmds.listAttr(objset, userDefined=True) or list(): + try: + value = cmds.getAttr("%s.%s" % (objset, attr)) + except Exception: + # Some attributes cannot be read directly, + # such as mesh and color attributes. These + # are considered non-essential to this + # particular publishing pipeline. + value = None + data[attr] = value + + # temporarily translation of `active` to `publish` till issue has + # been resolved, https://github.com/pyblish/pyblish-base/issues/307 + if "active" in data: + data["publish"] = data["active"] + + # Collect members + members = cmds.ls(members, long=True) or [] + + # `maya.cmds.listRelatives(noIntermediate=True)` only works when + # `shapes=True` argument is passed, since we also want to include + # transforms we filter afterwards. + children = cmds.listRelatives(members, + allDescendents=True, + fullPath=True) or [] + children = cmds.ls(children, noIntermediate=True, long=True) + + parents = [] + if data.get("includeParentHierarchy", True): + # If `includeParentHierarchy` then include the parents + # so they will also be picked up in the instance by validators + parents = self.get_all_parents(members) + members_hierarchy = list(set(members + children + parents)) + + if 'families' not in data: + data['families'] = [data.get('family')] + + # Create the instance + instance = context.create_instance(objset) + instance[:] = members_hierarchy + + # Store the exact members of the object set + instance.data["setMembers"] = members + + + # Define nice label + name = cmds.ls(objset, long=False)[0] # use short name + label = "{0} ({1})".format(name, + data["asset"]) + + # Append start frame and end frame to label if present + if "frameStart" and "frameEnd" in data: + label += " [{0}-{1}]".format(int(data["frameStart"]), + int(data["frameEnd"])) + + instance.data["label"] = label + + instance.data.update(data) + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.debug("DATA: \"%s\" " % instance.data) + + + def sort_by_family(instance): + """Sort by family""" + return instance.data.get("families", instance.data.get("family")) + + # Sort/grouped by family (preserving local index) + context[:] = sorted(context, key=sort_by_family) + + return context + + def get_all_parents(self, nodes): + """Get all parents by using string operations (optimization) + + Args: + nodes (list): the nodes which are found in the objectSet + + Returns: + list + """ + + parents = [] + for node in nodes: + splitted = node.split("|") + items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] + parents.extend(items) + + return list(set(parents)) diff --git a/pype/unreal/__init__.py b/pype/unreal/__init__.py index e69de29bb2..bb8a765a43 100644 --- a/pype/unreal/__init__.py +++ b/pype/unreal/__init__.py @@ -0,0 +1,45 @@ +import os +import logging + +from avalon import api as avalon +from pyblish import api as pyblish + +logger = logging.getLogger("pype.unreal") + +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "unreal", "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "unreal", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "unreal", "create") + + +def install(): + """Install Unreal configuration for Avalon.""" + print("-=" * 40) + logo = '''. +. + ____________ + / \\ __ \\ + \\ \\ \\/_\\ \\ + \\ \\ _____/ ______ + \\ \\ \\___// \\ \\ + \\ \\____\\ \\ \\_____\\ + \\/_____/ \\/______/ PYPE Club . +. +''' + print(logo) + print("installing Pype for Unreal ...") + print("-=" * 40) + logger.info("installing Pype for Unreal") + pyblish.register_plugin_path(str(PUBLISH_PATH)) + avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) + avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) + + +def uninstall(): + """Uninstall Unreal configuration for Avalon.""" + pyblish.deregister_plugin_path(str(PUBLISH_PATH)) + avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH)) + avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH)) diff --git a/pype/unreal/plugin.py b/pype/unreal/plugin.py new file mode 100644 index 0000000000..d403417ad1 --- /dev/null +++ b/pype/unreal/plugin.py @@ -0,0 +1,9 @@ +from avalon import api + + +class Creator(api.Creator): + pass + + +class Loader(api.Loader): + pass From ada2cc0f4e9b6a351c9cb9ae6b1d56ed1d37e631 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Mar 2020 21:43:47 +0100 Subject: [PATCH 100/327] base of unreal staticMesh pipeline --- .../global/publish/collect_scene_version.py | 2 +- pype/plugins/unreal/create/create_fbx.py | 14 -- .../unreal/create/create_staticmeshfbx.py | 33 ++++ .../plugins/unreal/load/load_staticmeshfbx.py | 52 +++++- .../unreal/publish/collect_instances.py | 149 ++++-------------- 5 files changed, 112 insertions(+), 138 deletions(-) delete mode 100644 pype/plugins/unreal/create/create_fbx.py create mode 100644 pype/plugins/unreal/create/create_staticmeshfbx.py diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 314a64f550..8c2bacf6e1 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -16,7 +16,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if "standalonepublisher" in context.data.get("host", []): return - if "unreal" in context.data.get("host", []): + if "unreal" in pyblish.api.registered_hosts(): return filename = os.path.basename(context.data.get('currentFile')) diff --git a/pype/plugins/unreal/create/create_fbx.py b/pype/plugins/unreal/create/create_fbx.py deleted file mode 100644 index 0d5b0bf316..0000000000 --- a/pype/plugins/unreal/create/create_fbx.py +++ /dev/null @@ -1,14 +0,0 @@ -from pype.unreal.plugin import Creator - - -class CreateFbx(Creator): - """Static FBX geometry""" - - name = "modelMain" - label = "Model" - family = "model" - icon = "cube" - asset_types = ["StaticMesh"] - - def __init__(self, *args, **kwargs): - super(CreateFbx, self).__init__(*args, **kwargs) diff --git a/pype/plugins/unreal/create/create_staticmeshfbx.py b/pype/plugins/unreal/create/create_staticmeshfbx.py new file mode 100644 index 0000000000..8002299f0a --- /dev/null +++ b/pype/plugins/unreal/create/create_staticmeshfbx.py @@ -0,0 +1,33 @@ +import unreal +from pype.unreal.plugin import Creator +from avalon.unreal import ( + instantiate, +) + + +class CreateStaticMeshFBX(Creator): + """Static FBX geometry""" + + name = "unrealStaticMeshMain" + label = "Unreal - Static Mesh" + family = "unrealStaticMesh" + icon = "cube" + asset_types = ["StaticMesh"] + + root = "/Game" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateStaticMeshFBX, self).__init__(*args, **kwargs) + + def process(self): + + name = self.data["subset"] + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + unreal.log("selection: {}".format(selection)) + instantiate(self.root, name, self.data, selection, self.suffix) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py index 056c81d54d..61e765f7c2 100644 --- a/pype/plugins/unreal/load/load_staticmeshfbx.py +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -1,7 +1,6 @@ from avalon import api from avalon import unreal as avalon_unreal import unreal -import time class StaticMeshFBXLoader(api.Loader): @@ -14,13 +13,33 @@ class StaticMeshFBXLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ tools = unreal.AssetToolsHelpers().get_asset_tools() temp_dir, temp_name = tools.create_unique_asset_name( "/Game/{}".format(name), "_TMP" ) - # asset_path = "/Game/{}".format(namespace) unreal.EditorAssetLibrary.make_directory(temp_dir) task = unreal.AssetImportTask() @@ -51,3 +70,32 @@ class StaticMeshFBXLoader(api.Loader): unreal.EditorAssetLibrary.delete_directory(temp_dir) return asset_content + + def update(self, container, representation): + node = container["objectName"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.filename = source_path + task.destination_path = destination_path + # strip suffix + task.destination_name = node[:-4] + task.replace_existing = True + task.automated = True + task.save = True + + task.options = unreal.FbxImportUI() + task.options.import_animations = False + + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + avalon_unreal.imprint( + container_path, {"_id": str(representation["_id"])}) + + def remove(self, container): + unreal.EditorAssetLibrary.delete_directory(container["namespace"]) diff --git a/pype/plugins/unreal/publish/collect_instances.py b/pype/plugins/unreal/publish/collect_instances.py index fa604f79d3..766a73028c 100644 --- a/pype/plugins/unreal/publish/collect_instances.py +++ b/pype/plugins/unreal/publish/collect_instances.py @@ -4,23 +4,14 @@ import pyblish.api class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by objectSet and pre-defined attribute + """Gather instances by AvalonPublishInstance class - This collector takes into account assets that are associated with - an objectSet and marked with a unique identifier; + This collector finds all paths containing `AvalonPublishInstance` class + asset Identifier: id (str): "pyblish.avalon.instance" - Limitations: - - Does not take into account nodes connected to those - within an objectSet. Extractors are assumed to export - with history preserved, but this limits what they will - be able to achieve and the amount of data available - to validators. An additional collector could also - append this input data into the instance, as we do - for `pype.rig` with collect_history. - """ label = "Collect Instances" @@ -29,124 +20,40 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): - objectset = cmds.ls("*.id", long=True, type="objectSet", - recursive=True, objectsOnly=True) + ar = unreal.AssetRegistryHelpers.get_asset_registry() + instance_containers = ar.get_assets_by_class( + "AvalonPublishInstance", True) - context.data['objectsets'] = objectset - for objset in objectset: + for container_data in instance_containers: + asset = container_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = container_data.asset_name + # convert to strings + data = {str(key): str(value) for (key, value) in data.items()} + assert data.get("family"), ( + "instance has no family" + ) - if not cmds.attributeQuery("id", node=objset, exists=True): - continue + # content of container + members = unreal.EditorAssetLibrary.list_assets( + asset.get_path_name(), recursive=True, include_folder=True + ) + self.log.debug(members) + self.log.debug(asset.get_path_name()) + # remove instance container + members.remove(asset.get_path_name()) + self.log.info("Creating instance for {}".format(asset.get_name())) - id_attr = "{}.id".format(objset) - if cmds.getAttr(id_attr) != "pyblish.avalon.instance": - continue - - # The developer is responsible for specifying - # the family of each instance. - has_family = cmds.attributeQuery("family", - node=objset, - exists=True) - assert has_family, "\"%s\" was missing a family" % objset - - members = cmds.sets(objset, query=True) - if members is None: - self.log.warning("Skipped empty instance: \"%s\" " % objset) - continue - - self.log.info("Creating instance for {}".format(objset)) - - data = dict() - - # Apply each user defined attribute as data - for attr in cmds.listAttr(objset, userDefined=True) or list(): - try: - value = cmds.getAttr("%s.%s" % (objset, attr)) - except Exception: - # Some attributes cannot be read directly, - # such as mesh and color attributes. These - # are considered non-essential to this - # particular publishing pipeline. - value = None - data[attr] = value - - # temporarily translation of `active` to `publish` till issue has - # been resolved, https://github.com/pyblish/pyblish-base/issues/307 - if "active" in data: - data["publish"] = data["active"] - - # Collect members - members = cmds.ls(members, long=True) or [] - - # `maya.cmds.listRelatives(noIntermediate=True)` only works when - # `shapes=True` argument is passed, since we also want to include - # transforms we filter afterwards. - children = cmds.listRelatives(members, - allDescendents=True, - fullPath=True) or [] - children = cmds.ls(children, noIntermediate=True, long=True) - - parents = [] - if data.get("includeParentHierarchy", True): - # If `includeParentHierarchy` then include the parents - # so they will also be picked up in the instance by validators - parents = self.get_all_parents(members) - members_hierarchy = list(set(members + children + parents)) - - if 'families' not in data: - data['families'] = [data.get('family')] - - # Create the instance - instance = context.create_instance(objset) - instance[:] = members_hierarchy + instance = context.create_instance(asset.get_name()) + instance[:] = members # Store the exact members of the object set instance.data["setMembers"] = members + instance.data["families"] = [data.get("family")] - - # Define nice label - name = cmds.ls(objset, long=False)[0] # use short name - label = "{0} ({1})".format(name, + label = "{0} ({1})".format(asset.get_name()[:-4], data["asset"]) - # Append start frame and end frame to label if present - if "frameStart" and "frameEnd" in data: - label += " [{0}-{1}]".format(int(data["frameStart"]), - int(data["frameEnd"])) - instance.data["label"] = label instance.data.update(data) - - # Produce diagnostic message for any graphical - # user interface interested in visualising it. - self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: \"%s\" " % instance.data) - - - def sort_by_family(instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=sort_by_family) - - return context - - def get_all_parents(self, nodes): - """Get all parents by using string operations (optimization) - - Args: - nodes (list): the nodes which are found in the objectSet - - Returns: - list - """ - - parents = [] - for node in nodes: - splitted = node.split("|") - items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] - parents.extend(items) - - return list(set(parents)) From a1a27784ae85f5c7da925059c4ff3f0b4ef66429 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 17 Mar 2020 00:01:35 +0100 Subject: [PATCH 101/327] updated docs --- pype/unreal/lib.py | 12 +++++++++++- pype/unreal/plugin.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index be6314b09b..faf3a6e99f 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -13,7 +13,17 @@ def get_engine_versions(): Location can be overridden by `UNREAL_ENGINE_LOCATION` environment variable. - Returns dictionary with version as a key and dir as value. + Returns: + + dict: dictionary with version as a key and dir as value. + + Example: + + >>> get_engine_version() + { + "4.23": "C:/Epic Games/UE_4.23", + "4.24": "C:/Epic Games/UE_4.24" + } """ try: engine_locations = {} diff --git a/pype/unreal/plugin.py b/pype/unreal/plugin.py index d403417ad1..0c00eb77d6 100644 --- a/pype/unreal/plugin.py +++ b/pype/unreal/plugin.py @@ -2,8 +2,10 @@ from avalon import api class Creator(api.Creator): + """This serves as skeleton for future Pype specific functionality""" pass class Loader(api.Loader): + """This serves as skeleton for future Pype specific functionality""" pass From 877b9e8885dc431775e716f4d96c4766b966335d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Mar 2020 17:27:09 +0100 Subject: [PATCH 102/327] feat(nuke): publish baked mov with preset colorspace --- pype/nuke/lib.py | 22 ++++++++++++++----- .../nuke/publish/extract_review_data_mov.py | 7 +++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 8e241dad16..f8284d18dd 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1564,10 +1564,9 @@ class ExporterReviewMov(ExporterReview): self.nodes = {} # deal with now lut defined in viewer lut - if hasattr(klass, "viewer_lut_raw"): - self.viewer_lut_raw = klass.viewer_lut_raw - else: - self.viewer_lut_raw = False + self.viewer_lut_raw = klass.viewer_lut_raw + self.bake_colorspace_fallback = klass.bake_colorspace_fallback + self.bake_colorspace_main = klass.bake_colorspace_main self.name = name or "baked" self.ext = ext or "mov" @@ -1628,8 +1627,19 @@ class ExporterReviewMov(ExporterReview): self.log.debug("ViewProcess... `{}`".format(self._temp_nodes)) if not self.viewer_lut_raw: - # OCIODisplay node - dag_node = nuke.createNode("OCIODisplay") + colorspace = self.bake_colorspace_main \ + or self.bake_colorspace_fallback + + self.log.debug("_ colorspace... `{}`".format(colorspace)) + + if colorspace: + # OCIOColorSpace with controled output + dag_node = nuke.createNode("OCIOColorSpace") + dag_node["out_colorspace"].setValue(str(colorspace)) + else: + # OCIODisplay + dag_node = nuke.createNode("OCIODisplay") + # connect dag_node.setInput(0, self.previous_node) self._temp_nodes.append(dag_node) diff --git a/pype/plugins/nuke/publish/extract_review_data_mov.py b/pype/plugins/nuke/publish/extract_review_data_mov.py index 8b204680a7..1c6efafcfe 100644 --- a/pype/plugins/nuke/publish/extract_review_data_mov.py +++ b/pype/plugins/nuke/publish/extract_review_data_mov.py @@ -3,7 +3,7 @@ import pyblish.api from avalon.nuke import lib as anlib from pype.nuke import lib as pnlib import pype - +reload(pnlib) class ExtractReviewDataMov(pype.api.Extractor): """Extracts movie and thumbnail with baked in luts @@ -18,6 +18,11 @@ class ExtractReviewDataMov(pype.api.Extractor): families = ["review", "render", "render.local"] hosts = ["nuke"] + # presets + viewer_lut_raw = None + bake_colorspace_fallback = None + bake_colorspace_main = None + def process(self, instance): families = instance.data["families"] self.log.info("Creating staging dir...") From 94ce045fec85e82d45e8887cd3bbb7f6d717ee12 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Mar 2020 18:32:59 +0100 Subject: [PATCH 103/327] fix(nuke): fallback approach of defining colorspace --- pype/nuke/lib.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index f8284d18dd..446f9af6a3 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1627,15 +1627,22 @@ class ExporterReviewMov(ExporterReview): self.log.debug("ViewProcess... `{}`".format(self._temp_nodes)) if not self.viewer_lut_raw: - colorspace = self.bake_colorspace_main \ - or self.bake_colorspace_fallback + colorspaces = [ + self.bake_colorspace_main, self.bake_colorspace_fallback + ] - self.log.debug("_ colorspace... `{}`".format(colorspace)) - - if colorspace: + if any(colorspaces): # OCIOColorSpace with controled output dag_node = nuke.createNode("OCIOColorSpace") - dag_node["out_colorspace"].setValue(str(colorspace)) + for c in colorspaces: + test = dag_node["out_colorspace"].setValue(str(c)) + if test: + self.log.info( + "Baking in colorspace... `{}`".format(c)) + break + + if not test: + dag_node = nuke.createNode("OCIODisplay") else: # OCIODisplay dag_node = nuke.createNode("OCIODisplay") From 3bf653b006e336734cfecb421964cc2226e61717 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Mar 2020 11:25:13 +0100 Subject: [PATCH 104/327] fixed bug with cpp project setup --- pype/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index faf3a6e99f..8f87fdbf4e 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -257,7 +257,7 @@ def create_unreal_project(project_name: str, "pip", "install", "pyside"]) if dev_mode or preset["dev_mode"]: - _prepare_cpp_project(pr_dir, engine_path) + _prepare_cpp_project(project_file, engine_path) def _prepare_cpp_project(project_file: str, engine_path: str) -> None: From 6ae5e2d8a54c76240169d1cb3d5513738448a48d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 15:13:57 +0100 Subject: [PATCH 105/327] fix(nuke): separate write render and prerender create plugins --- ...ate_write.py => create_write_prerender.py} | 107 ++---------------- .../nuke/create/create_write_render.py | 101 +++++++++++++++++ 2 files changed, 108 insertions(+), 100 deletions(-) rename pype/plugins/nuke/create/{create_write.py => create_write_prerender.py} (54%) create mode 100644 pype/plugins/nuke/create/create_write_render.py diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write_prerender.py similarity index 54% rename from pype/plugins/nuke/create/create_write.py rename to pype/plugins/nuke/create/create_write_prerender.py index 74e450f267..f8210db9db 100644 --- a/pype/plugins/nuke/create/create_write.py +++ b/pype/plugins/nuke/create/create_write_prerender.py @@ -1,103 +1,11 @@ from collections import OrderedDict -from pype.nuke import plugin +from pype.nuke import ( + plugin, + lib as pnlib + ) import nuke -class CreateWriteRender(plugin.PypeCreator): - # change this to template preset - name = "WriteRender" - label = "Create Write Render" - hosts = ["nuke"] - n_class = "write" - family = "render" - icon = "sign-out" - defaults = ["Main", "Mask"] - - def __init__(self, *args, **kwargs): - super(CreateWriteRender, self).__init__(*args, **kwargs) - - data = OrderedDict() - - data["family"] = self.family - data["families"] = self.n_class - - for k, v in self.data.items(): - if k not in data.keys(): - data.update({k: v}) - - self.data = data - self.nodes = nuke.selectedNodes() - self.log.debug("_ self.data: '{}'".format(self.data)) - - def process(self): - from pype.nuke import lib as pnlib - - inputs = [] - outputs = [] - instance = nuke.toNode(self.data["subset"]) - selected_node = None - - # use selection - if (self.options or {}).get("useSelection"): - nodes = self.nodes - - if not (len(nodes) < 2): - msg = ("Select only one node. The node you want to connect to, " - "or tick off `Use selection`") - log.error(msg) - nuke.message(msg) - - selected_node = nodes[0] - inputs = [selected_node] - outputs = selected_node.dependent() - - if instance: - if (instance.name() in selected_node.name()): - selected_node = instance.dependencies()[0] - - # if node already exist - if instance: - # collect input / outputs - inputs = instance.dependencies() - outputs = instance.dependent() - selected_node = inputs[0] - # remove old one - nuke.delete(instance) - - # recreate new - write_data = { - "class": self.n_class, - "families": [self.family], - "avalon": self.data - } - - if self.presets.get('fpath_template'): - self.log.info("Adding template path from preset") - write_data.update( - {"fpath_template": self.presets["fpath_template"]} - ) - else: - self.log.info("Adding template path from plugin") - write_data.update({ - "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}"}) - - write_node = pnlib.create_write_node( - self.data["subset"], - write_data, - input=selected_node) - - # relinking to collected connections - for i, input in enumerate(inputs): - write_node.setInput(i, input) - - write_node.autoplace() - - for output in outputs: - output.setInput(0, write_node) - - return write_node - - class CreateWritePrerender(plugin.PypeCreator): # change this to template preset name = "WritePrerender" @@ -125,8 +33,6 @@ class CreateWritePrerender(plugin.PypeCreator): self.log.debug("_ self.data: '{}'".format(self.data)) def process(self): - from pype.nuke import lib as pnlib - inputs = [] outputs = [] instance = nuke.toNode(self.data["subset"]) @@ -137,8 +43,9 @@ class CreateWritePrerender(plugin.PypeCreator): nodes = self.nodes if not (len(nodes) < 2): - msg = ("Select only one node. The node you want to connect to, " - "or tick off `Use selection`") + msg = ("Select only one node. The node " + "you want to connect to, " + "or tick off `Use selection`") self.log.error(msg) nuke.message(msg) diff --git a/pype/plugins/nuke/create/create_write_render.py b/pype/plugins/nuke/create/create_write_render.py new file mode 100644 index 0000000000..c3b60ba2b0 --- /dev/null +++ b/pype/plugins/nuke/create/create_write_render.py @@ -0,0 +1,101 @@ +from collections import OrderedDict +from pype.nuke import ( + plugin, + lib as pnlib + ) +import nuke + + +class CreateWriteRender(plugin.PypeCreator): + # change this to template preset + name = "WriteRender" + label = "Create Write Render" + hosts = ["nuke"] + n_class = "write" + family = "render" + icon = "sign-out" + defaults = ["Main", "Mask"] + + def __init__(self, *args, **kwargs): + super(CreateWriteRender, self).__init__(*args, **kwargs) + + data = OrderedDict() + + data["family"] = self.family + data["families"] = self.n_class + + for k, v in self.data.items(): + if k not in data.keys(): + data.update({k: v}) + + self.data = data + self.nodes = nuke.selectedNodes() + self.log.debug("_ self.data: '{}'".format(self.data)) + + def process(self): + + inputs = [] + outputs = [] + instance = nuke.toNode(self.data["subset"]) + selected_node = None + + # use selection + if (self.options or {}).get("useSelection"): + nodes = self.nodes + + if not (len(nodes) < 2): + msg = ("Select only one node. " + "The node you want to connect to, " + "or tick off `Use selection`") + self.log.error(msg) + nuke.message(msg) + + selected_node = nodes[0] + inputs = [selected_node] + outputs = selected_node.dependent() + + if instance: + if (instance.name() in selected_node.name()): + selected_node = instance.dependencies()[0] + + # if node already exist + if instance: + # collect input / outputs + inputs = instance.dependencies() + outputs = instance.dependent() + selected_node = inputs[0] + # remove old one + nuke.delete(instance) + + # recreate new + write_data = { + "class": self.n_class, + "families": [self.family], + "avalon": self.data + } + + if self.presets.get('fpath_template'): + self.log.info("Adding template path from preset") + write_data.update( + {"fpath_template": self.presets["fpath_template"]} + ) + else: + self.log.info("Adding template path from plugin") + write_data.update({ + "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}"}) + + write_node = pnlib.create_write_node( + self.data["subset"], + write_data, + input=selected_node) + + # relinking to collected connections + for i, input in enumerate(inputs): + write_node.setInput(i, input) + + write_node.autoplace() + + for output in outputs: + output.setInput(0, write_node) + + return write_node From 5dfbe54df1b33b6391dc1b7bf718495982da234d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:01:09 +0100 Subject: [PATCH 106/327] clean(nuke): old code --- pype/plugins/nuke/publish/collect_instances.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index cbbef70e4a..57b4208ce4 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -89,8 +89,6 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): instance.append(i) node.end() - family = avalon_knob_data["family"] - families = list() families_ak = avalon_knob_data.get("families") if families_ak: From a4938110c2c7025f74fe4686a1c4de51706359e1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:01:47 +0100 Subject: [PATCH 107/327] feat(nuke): accepting `prerender` family on instance --- pype/plugins/nuke/publish/collect_instances.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 57b4208ce4..893c6db7e5 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -80,6 +80,8 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): add_family = "render.farm" instance.data["transfer"] = False families.append(add_family) + if "prerender" in family: + families.append("prerender") else: # add family into families families.insert(0, family) From 7bbc0e5677169091c34611cf9b657ce871de402e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:02:24 +0100 Subject: [PATCH 108/327] clean(nuke): write family is more direct and lest anatomical --- pype/plugins/nuke/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/collect_review.py b/pype/plugins/nuke/publish/collect_review.py index c95c94541d..f02f22e053 100644 --- a/pype/plugins/nuke/publish/collect_review.py +++ b/pype/plugins/nuke/publish/collect_review.py @@ -9,7 +9,7 @@ class CollectReview(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.3 label = "Collect Review" hosts = ["nuke"] - families = ["render", "render.local", "render.farm"] + families = ["write", "prerender"] def process(self, instance): From d9a2c7dad5dac122fe1194dc3bf0e02cd9ebac0c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:02:46 +0100 Subject: [PATCH 109/327] clean(nuke): not used import --- pype/plugins/nuke/publish/collect_writes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 0dc7c81fae..a6fbdbab8b 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,7 +1,6 @@ import os import nuke import pyblish.api -import pype.api as pype @pyblish.api.log From 3d32068c10b8e51f1e05d90fdb75485cde312ed2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:06:13 +0100 Subject: [PATCH 110/327] feat(nuke): accepting prerender in img collection --- pype/plugins/nuke/publish/collect_writes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index a6fbdbab8b..b1213199f5 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -67,6 +67,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): ) if 'render' in instance.data['families']: + if [fm for fm in instance.data['families'] + if fm in ["render", "prerender"]]: if "representations" not in instance.data: instance.data["representations"] = list() From e2f51960452f29bb8e717324415c45aa638ba5bf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:06:41 +0100 Subject: [PATCH 111/327] clean(nuke): removing wrong family definition --- pype/plugins/nuke/publish/validate_write_bounding_box.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/validate_write_bounding_box.py b/pype/plugins/nuke/publish/validate_write_bounding_box.py index e4b7c77a25..cedeea6d9f 100644 --- a/pype/plugins/nuke/publish/validate_write_bounding_box.py +++ b/pype/plugins/nuke/publish/validate_write_bounding_box.py @@ -57,7 +57,7 @@ class ValidateNukeWriteBoundingBox(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder optional = True - families = ["render", "render.local", "render.farm"] + families = ["write"] label = "Write Bounding Box" hosts = ["nuke"] actions = [RepairNukeBoundingBoxAction] From 8c8a4e11ab9a17c2319569d64673f20cc15c9773 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:09:05 +0100 Subject: [PATCH 112/327] feat(nuke): prerender family clarifying --- pype/plugins/nuke/publish/collect_writes.py | 22 ++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index b1213199f5..aa5f825a98 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -66,7 +66,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): int(last_frame) ) - if 'render' in instance.data['families']: if [fm for fm in instance.data['families'] if fm in ["render", "prerender"]]: if "representations" not in instance.data: @@ -96,7 +95,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): # this will only run if slate frame is not already # rendered from previews publishes if "slate" in instance.data["families"] \ - and (frame_length == collected_frames_len): + and (frame_length == collected_frames_len) \ + and ("prerender" not in instance.data["families"]): frame_slate_str = "%0{}d".format( len(str(last_frame))) % (first_frame - 1) slate_frame = collected_frames[0].replace( @@ -105,6 +105,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): representation['files'] = collected_frames instance.data["representations"].append(representation) + if "render" not in instance.data["families"]: + instance.data["families"].append("render") except Exception: instance.data["representations"].append(representation) self.log.debug("couldn't collect frames: {}".format(label)) @@ -144,5 +146,19 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) - + self.log.debug("families: {}".format(families)) + if "prerender" in families: + _families = list() + for fm in families: + if fm in _families: + continue + if "render" in fm: + if "prerender" in fm: + continue + _families.append(fm) + instance.data.update({ + "family": "prerender", + "families": _families + }) + self.log.debug("_families: {}".format(_families)) self.log.debug("instance.data: {}".format(instance.data)) From c7aba1564aab06258841581cd423047c838e4a53 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Mar 2020 19:14:08 +0100 Subject: [PATCH 113/327] group AOVs from maya render --- .../global/publish/submit_publish_job.py | 5 +++-- pype/plugins/maya/publish/collect_render.py | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index dcf19ae32c..e517198ba2 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -170,7 +170,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "review": ["lutPath"], "render.farm": ["bakeScriptPath", "bakeRenderPath", "bakeWriteNodeName", "version"] - } + } # list of family names to transfer to new family if present families_transfer = ["render3d", "render2d", "ftrack", "slate"] @@ -276,7 +276,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # if override remove all frames we are expecting to be rendered # so we'll copy only those missing from current render if instance.data.get("overrideExistingFrame"): - for frame in range(start, end+1): + for frame in range(start, end + 1): if frame not in r_col.indexes: continue r_col.indexes.remove(frame) @@ -366,6 +366,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = copy(instance_data) new_instance["subset"] = subset_name + new_instance["group"] = aov ext = cols[0].tail.lstrip(".") diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index be3878e6bd..8d74d242b3 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -211,19 +211,23 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "attachTo": attachTo, "setMembers": layer_name, "publish": True, - "frameStart": int(context.data["assetEntity"]['data']['frameStart']), - "frameEnd": int(context.data["assetEntity"]['data']['frameEnd']), - "frameStartHandle": int(self.get_render_attribute("startFrame", - layer=layer_name)), - "frameEndHandle": int(self.get_render_attribute("endFrame", - layer=layer_name)), + "frameStart": int( + context.data["assetEntity"]['data']['frameStart']), + "frameEnd": int( + context.data["assetEntity"]['data']['frameEnd']), + "frameStartHandle": int( + self.get_render_attribute("startFrame", layer=layer_name)), + "frameEndHandle": int( + self.get_render_attribute("endFrame", layer=layer_name)), "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), "renderer": self.get_render_attribute("currentRenderer", layer=layer_name), - "handleStart": int(context.data["assetEntity"]['data']['handleStart']), - "handleEnd": int(context.data["assetEntity"]['data']['handleEnd']), + "handleStart": int( + context.data["assetEntity"]['data']['handleStart']), + "handleEnd": int( + context.data["assetEntity"]['data']['handleEnd']), # instance subset "family": "renderlayer", From 9248e4b8cf95c467560259f02126ef0d531ab3ac Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Mar 2020 19:15:04 +0100 Subject: [PATCH 114/327] clear(nuke): cleaning wrong families --- pype/plugins/nuke/publish/extract_review_data_mov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/extract_review_data_mov.py b/pype/plugins/nuke/publish/extract_review_data_mov.py index 8b204680a7..683da24fc8 100644 --- a/pype/plugins/nuke/publish/extract_review_data_mov.py +++ b/pype/plugins/nuke/publish/extract_review_data_mov.py @@ -15,7 +15,7 @@ class ExtractReviewDataMov(pype.api.Extractor): order = pyblish.api.ExtractorOrder + 0.01 label = "Extract Review Data Mov" - families = ["review", "render", "render.local"] + families = ["review"] hosts = ["nuke"] def process(self, instance): From 3c4e427b135bde7f2d8203462e74c287bedd7664 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 18 Mar 2020 20:28:43 +0100 Subject: [PATCH 115/327] store popen to variable --- pype/ftrack/lib/ftrack_app_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 2b46dd43d8..eebffda280 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -286,7 +286,9 @@ class AppAction(BaseHandler): # Run SW if was found executable if execfile is not None: - avalonlib.launch(executable=execfile, args=[], environment=env) + popen = avalonlib.launch( + executable=execfile, args=[], environment=env + ) else: return { 'success': False, From 7f035d146f580b7591238d7d5491f09692eb61a1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 18 Mar 2020 20:28:57 +0100 Subject: [PATCH 116/327] blender init cleanup --- pype/blender/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pype/blender/__init__.py b/pype/blender/__init__.py index 8a29917e40..4b6074a820 100644 --- a/pype/blender/__init__.py +++ b/pype/blender/__init__.py @@ -1,16 +1,8 @@ -import logging -from pathlib import Path import os -import bpy - from avalon import api as avalon from pyblish import api as pyblish -from .plugin import AssetLoader - -logger = logging.getLogger("pype.blender") - PARENT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.dirname(PARENT_DIR) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") From fcc26cd9f7d36d823e42656fa6f2a9952c8efdf3 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:47:05 +0100 Subject: [PATCH 117/327] add all frame data to context and fix order --- .../global/publish/collect_avalon_entities.py | 11 ++++++++++- .../global/publish/collect_rendered_files.py | 2 +- pype/plugins/maya/publish/collect_scene.py | 14 ++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 103f5abd1a..53f11aa693 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -15,7 +15,7 @@ import pyblish.api class CollectAvalonEntities(pyblish.api.ContextPlugin): """Collect Anatomy into Context""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.02 label = "Collect Avalon Entities" def process(self, context): @@ -47,7 +47,16 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["assetEntity"] = asset_entity data = asset_entity['data'] + + context.data["frameStart"] = data.get("frameStart") + context.data["frameEnd"] = data.get("frameEnd") + handles = int(data.get("handles") or 0) context.data["handles"] = handles context.data["handleStart"] = int(data.get("handleStart", handles)) context.data["handleEnd"] = int(data.get("handleEnd", handles)) + + frame_start_h = data.get("frameStart") - context.data["handleStart"] + frame_end_h = data.get("frameEnd") + context.data["handleEnd"] + context.data["frameStartHandle"] = frame_start_h + context.data["frameEndHandle"] = frame_end_h diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index 552fd49f6d..8ecf7ba156 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -13,7 +13,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): `PYPE_PUBLISH_DATA`. Those files _MUST_ share same context. """ - order = pyblish.api.CollectorOrder - 0.0001 + order = pyblish.api.CollectorOrder - 0.1 targets = ["filesequence"] label = "Collect rendered frames" diff --git a/pype/plugins/maya/publish/collect_scene.py b/pype/plugins/maya/publish/collect_scene.py index 089019f2d3..e6976356e8 100644 --- a/pype/plugins/maya/publish/collect_scene.py +++ b/pype/plugins/maya/publish/collect_scene.py @@ -9,13 +9,14 @@ from pype.maya import lib class CollectMayaScene(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.1 + order = pyblish.api.CollectorOrder - 0.01 label = "Maya Workfile" hosts = ['maya'] def process(self, context): """Inject the current working file""" - current_file = context.data['currentFile'] + current_file = cmds.file(query=True, sceneName=True) + context.data['currentFile'] = current_file folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) @@ -24,9 +25,6 @@ class CollectMayaScene(pyblish.api.ContextPlugin): data = {} - for key, value in lib.collect_animation_data().items(): - data[key] = value - # create instance instance = context.create_instance(name=filename) subset = 'workfile' + task.capitalize() @@ -38,7 +36,11 @@ class CollectMayaScene(pyblish.api.ContextPlugin): "publish": True, "family": 'workfile', "families": ['workfile'], - "setMembers": [current_file] + "setMembers": [current_file], + "frameStart": context.data['frameStart'], + "frameEnd": context.data['frameEnd'], + "handleStart": context.data['handleStart'], + "handleEnd": context.data['handleEnd'] }) data['representations'] = [{ From 4778dbd3e6aa48d11c653f429e87018bdd55d1e4 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:47:37 +0100 Subject: [PATCH 118/327] remove handles from custom pointcache range --- pype/plugins/maya/publish/extract_pointcache.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/publish/extract_pointcache.py b/pype/plugins/maya/publish/extract_pointcache.py index cec4886712..e40ab6e7da 100644 --- a/pype/plugins/maya/publish/extract_pointcache.py +++ b/pype/plugins/maya/publish/extract_pointcache.py @@ -25,12 +25,8 @@ class ExtractAlembic(pype.api.Extractor): nodes = instance[:] # Collect the start and end including handles - start = instance.data.get("frameStart", 1) - end = instance.data.get("frameEnd", 1) - handles = instance.data.get("handles", 0) - if handles: - start -= handles - end += handles + start = float(instance.data.get("frameStartHandle", 1)) + end = float(instance.data.get("frameEndHandle", 1)) attrs = instance.data.get("attr", "").split(";") attrs = [value for value in attrs if value.strip()] From b329fde9835df94a56def669c2bb60ceb913f473 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:48:14 +0100 Subject: [PATCH 119/327] use custom frame range if other than asset --- .../plugins/maya/publish/collect_instances.py | 44 ++++++++++++++++--- pype/plugins/maya/publish/collect_render.py | 41 ++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/pype/plugins/maya/publish/collect_instances.py b/pype/plugins/maya/publish/collect_instances.py index 5af717ba4d..9ea3ebe7fa 100644 --- a/pype/plugins/maya/publish/collect_instances.py +++ b/pype/plugins/maya/publish/collect_instances.py @@ -1,6 +1,7 @@ from maya import cmds import pyblish.api +import json class CollectInstances(pyblish.api.ContextPlugin): @@ -32,6 +33,13 @@ class CollectInstances(pyblish.api.ContextPlugin): objectset = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) + ctx_frame_start = context.data['frameStart'] + ctx_frame_end = context.data['frameEnd'] + ctx_handle_start = context.data['handleStart'] + ctx_handle_end = context.data['handleEnd'] + ctx_frame_start_handle = context.data['frameStartHandle'] + ctx_frame_end_handle = context.data['frameEndHandle'] + context.data['objectsets'] = objectset for objset in objectset: @@ -108,14 +116,36 @@ class CollectInstances(pyblish.api.ContextPlugin): label = "{0} ({1})".format(name, data["asset"]) - if "handles" in data: - data["handleStart"] = data["handles"] - data["handleEnd"] = data["handles"] - # Append start frame and end frame to label if present if "frameStart" and "frameEnd" in data: - data["frameStartHandle"] = data["frameStart"] - data["handleStart"] - data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + + # if frame range on maya set is the same as full shot range + # adjust the values to match the asset data + if (ctx_frame_start_handle == data["frameStart"] + and ctx_frame_end_handle == data["frameEnd"]): + data["frameStartHandle"] = ctx_frame_start_handle + data["frameEndHandle"] = ctx_frame_end_handle + data["frameStart"] = ctx_frame_start + data["frameEnd"] = ctx_frame_end + data["handleStart"] = ctx_handle_start + data["handleEnd"] = ctx_handle_end + + # if there are user values on start and end frame not matching + # the asset, use them + + else: + if "handles" in data: + data["handleStart"] = data["handles"] + data["handleEnd"] = data["handles"] + else: + data["handleStart"] = 0 + data["handleEnd"] = 0 + + data["frameStartHandle"] = data["frameStart"] - data["handleStart"] + data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + + if "handles" in data: + data.pop('handles') label += " [{0}-{1}]".format(int(data["frameStartHandle"]), int(data["frameEndHandle"])) @@ -127,7 +157,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: \"%s\" " % instance.data) + self.log.debug("DATA: {} ".format(json.dumps(instance.data, indent=4))) def sort_by_family(instance): """Sort by family""" diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index be3878e6bd..88c1be477d 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -41,6 +41,7 @@ import re import os import types import six +import json from abc import ABCMeta, abstractmethod from maya import cmds @@ -202,6 +203,28 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths.append(full_path) aov_dict["beauty"] = full_paths + frame_start_render = int(self.get_render_attribute("startFrame", + layer=layer_name)) + frame_end_render = int(self.get_render_attribute("endFrame", + layer=layer_name)) + + if (int(context.data['frameStartHandle']) == frame_start_render and + int(context.data['frameEndHandle']) == frame_end_render): + + handle_start = context.data['handleStart'] + handle_end = context.data['handleEnd'] + frame_start = context.data['frameStart'] + frame_end = context.data['frameEnd'] + frame_start_handle = context.data['frameStartHandle'] + frame_end_handle = context.data['frameEndHandle'] + else: + handle_start = 0 + handle_end = 0 + frame_start = frame_start_render + frame_end = frame_end_render + frame_start_handle = frame_start_render + frame_end_handle = frame_end_render + full_exp_files.append(aov_dict) self.log.info(full_exp_files) self.log.info("collecting layer: {}".format(layer_name)) @@ -211,20 +234,18 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "attachTo": attachTo, "setMembers": layer_name, "publish": True, - "frameStart": int(context.data["assetEntity"]['data']['frameStart']), - "frameEnd": int(context.data["assetEntity"]['data']['frameEnd']), - "frameStartHandle": int(self.get_render_attribute("startFrame", - layer=layer_name)), - "frameEndHandle": int(self.get_render_attribute("endFrame", - layer=layer_name)), + + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_start_handle, + "frameEndHandle": frame_end_handle, "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), "renderer": self.get_render_attribute("currentRenderer", layer=layer_name), - "handleStart": int(context.data["assetEntity"]['data']['handleStart']), - "handleEnd": int(context.data["assetEntity"]['data']['handleEnd']), - # instance subset "family": "renderlayer", "families": ["renderlayer"], @@ -267,7 +288,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance = context.create_instance(expected_layer_name) instance.data["label"] = label instance.data.update(data) - pass + self.log.debug("data: {}".format(json.dumps(data, indent=4))) def parse_options(self, render_globals): """Get all overrides with a value, skip those without From 5649b112f1772e6acf8544829b380e48aca29268 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:48:34 +0100 Subject: [PATCH 120/327] remove unnecessary current scene collector --- .../plugins/maya/publish/collect_current_file.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 pype/plugins/maya/publish/collect_current_file.py diff --git a/pype/plugins/maya/publish/collect_current_file.py b/pype/plugins/maya/publish/collect_current_file.py deleted file mode 100644 index 0b38ebcf3d..0000000000 --- a/pype/plugins/maya/publish/collect_current_file.py +++ /dev/null @@ -1,16 +0,0 @@ -from maya import cmds - -import pyblish.api - - -class CollectMayaCurrentFile(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - order = pyblish.api.CollectorOrder - 0.5 - label = "Maya Current File" - hosts = ['maya'] - - def process(self, context): - """Inject the current working file""" - current_file = cmds.file(query=True, sceneName=True) - context.data['currentFile'] = current_file From c73059869ec088f93dc2082e3a896f397e196bf5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 10:34:37 +0100 Subject: [PATCH 121/327] grammar fixes --- pype/plugins/global/publish/integrate_thumbnail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_thumbnail.py b/pype/plugins/global/publish/integrate_thumbnail.py index 0bb34eab58..97122d2c39 100644 --- a/pype/plugins/global/publish/integrate_thumbnail.py +++ b/pype/plugins/global/publish/integrate_thumbnail.py @@ -34,7 +34,7 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): published_repres = instance.data.get("published_representations") if not published_repres: self.log.debug( - "There are not published representations on the instance." + "There are no published representations on the instance." ) return @@ -42,12 +42,12 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] if "publish" not in anatomy.templates: - self.warning("Anatomy does not have set publish key!") + self.log.warning("Anatomy is missing the \"publish\" key!") return if "thumbnail" not in anatomy.templates["publish"]: - self.warning(( - "There is not set \"thumbnail\" template for project \"{}\"" + self.log.warning(( + "There is no \"thumbnail\" template set for the project \"{}\"" ).format(project_name)) return From 1ac0961f3abc10d4aa963b253b9eb177dbb9b3be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 10:36:37 +0100 Subject: [PATCH 122/327] added missing version_id to master version schema --- schema/master_version-1.0.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/schema/master_version-1.0.json b/schema/master_version-1.0.json index 991594648b..9dff570b3a 100644 --- a/schema/master_version-1.0.json +++ b/schema/master_version-1.0.json @@ -9,6 +9,7 @@ "additionalProperties": true, "required": [ + "version_id", "schema", "type", "parent" @@ -19,6 +20,10 @@ "description": "Document's id (database will create it's if not entered)", "example": "ObjectId(592c33475f8c1b064c4d1696)" }, + "version_id": { + "description": "The version ID from which it was created", + "example": "ObjectId(592c33475f8c1b064c4d1695)" + }, "schema": { "description": "The schema associated with this document", "type": "string", From a364b90ae330d2a3691cb219e8f5df83497c5373 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 10:50:48 +0100 Subject: [PATCH 123/327] integrate_new store subset and version entity to instance.data --- pype/plugins/global/publish/integrate_master_version.py | 5 +---- pype/plugins/global/publish/integrate_new.py | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 715d99c1c8..1cee7d1f24 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -61,13 +61,10 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): master_publish_dir = self.get_publish_dir(instance) - src_version_entity = None + src_version_entity = instance.data.get("versionEntity") filtered_repre_ids = [] for repre_id, repre_info in published_repres.items(): repre = repre_info["representation"] - if src_version_entity is None: - src_version_entity = repre_info.get("version_entity") - if repre["name"].lower() in self.ignored_representation_names: self.log.debug( "Filtering representation with name: `{}`".format( diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 8c27ccfa84..71a045a004 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -162,6 +162,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) subset = self.get_subset(asset_entity, instance) + instance.data["subsetEntity"] = subset version_number = instance.data["version"] self.log.debug("Next version: v{}".format(version_number)) @@ -237,6 +238,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) version = io.find_one({"_id": version_id}) + instance.data["versionEntity"] = version existing_repres = list(io.find({ "parent": version_id, @@ -463,10 +465,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): published_representations[repre_id] = { "representation": representation, "anatomy_data": template_data, - "published_files": published_files, - # TODO prabably should store subset and version to instance - "subset_entity": subset, - "version_entity": version + "published_files": published_files } self.log.debug("__ representations: {}".format(representations)) From 7d614c616daf65f5a384e549fa2202d3b3f5079e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 11:10:43 +0100 Subject: [PATCH 124/327] fixed bugs in itegrate master version when publishing first version --- pype/plugins/global/publish/integrate_master_version.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 1cee7d1f24..4600a95aa4 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -202,6 +202,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): repre_name_low = repre["name"].lower() archived_repres_by_name[repre_name_low] = repre + backup_master_publish_dir = str(master_publish_dir) if os.path.exists(master_publish_dir): backup_master_publish_dir = master_publish_dir + ".BACKUP" max_idx = 10 @@ -401,10 +402,12 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ) # Remove backuped previous master - shutil.rmtree(backup_master_publish_dir) + if os.path.exists(backup_master_publish_dir): + shutil.rmtree(backup_master_publish_dir) except Exception: - os.rename(backup_master_publish_dir, master_publish_dir) + if os.path.exists(backup_master_publish_dir): + os.rename(backup_master_publish_dir, master_publish_dir) self.log.error(( "!!! Creating of Master version failed." " Previous master version maybe lost some data!" From 2b384bcfca2a9e6d80b9a564445b010407f1a9a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 11:12:31 +0100 Subject: [PATCH 125/327] more specific validations of previous fix --- .../global/publish/integrate_master_version.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 4600a95aa4..0eba275407 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -202,7 +202,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): repre_name_low = repre["name"].lower() archived_repres_by_name[repre_name_low] = repre - backup_master_publish_dir = str(master_publish_dir) + backup_master_publish_dir = None if os.path.exists(master_publish_dir): backup_master_publish_dir = master_publish_dir + ".BACKUP" max_idx = 10 @@ -402,11 +402,17 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): ) # Remove backuped previous master - if os.path.exists(backup_master_publish_dir): + if ( + backup_master_publish_dir is not None and + os.path.exists(backup_master_publish_dir) + ): shutil.rmtree(backup_master_publish_dir) except Exception: - if os.path.exists(backup_master_publish_dir): + if ( + backup_master_publish_dir is not None and + os.path.exists(backup_master_publish_dir) + ): os.rename(backup_master_publish_dir, master_publish_dir) self.log.error(( "!!! Creating of Master version failed." From 61df87ff4bb3e9cd53391a1be2524f3bf372a0ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 12:51:58 +0100 Subject: [PATCH 126/327] hopefully intent is backwards compatible --- .../publish/integrate_ftrack_instances.py | 5 ++++- .../ftrack/publish/integrate_ftrack_note.py | 9 +++++++-- pype/plugins/global/publish/extract_burnin.py | 9 ++++++--- pype/plugins/global/publish/integrate_new.py | 18 ++++++++++++------ .../nuke/publish/extract_slate_frame.py | 6 ++++-- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index 591dcf0dc2..db257e901a 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -127,7 +127,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Add custom attributes for AssetVersion assetversion_cust_attrs = {} - intent_val = instance.context.data.get("intent", {}).get("value") + intent_val = instance.context.data.get("intent") + if intent_val and isinstance(intent_val, dict): + intent_val = intent_val.get("value") + if intent_val: assetversion_cust_attrs["intent"] = intent_val diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_note.py b/pype/plugins/ftrack/publish/integrate_ftrack_note.py index 679010ca58..9566207145 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_note.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_note.py @@ -71,8 +71,13 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): session = instance.context.data["ftrackSession"] - intent_val = instance.context.data.get("intent", {}).get("value") - intent_label = instance.context.data.get("intent", {}).get("label") + intent = instance.context.data.get("intent") + if intent and isinstance(intent, dict): + intent_val = intent.get("value") + intent_label = intent.get("label") + else: + intent_val = intent_label = intent + final_label = None if intent_val: final_label = self.get_intent_label(session, intent_val) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 086a1fdfb2..71463e296e 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -54,9 +54,12 @@ class ExtractBurnin(pype.api.Extractor): "comment": instance.context.data.get("comment", "") }) - intent = instance.context.data.get("intent", {}).get("label") - if intent: - prep_data["intent"] = intent + intent_label = instance.context.data.get("intent") + if intent_label and isinstance(intent_label, dict): + intent_label = intent_label.get("label") + + if intent_label: + prep_data["intent"] = intent_label # get anatomy project anatomy = instance.context.data['anatomy'] diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index aa214f36cb..ccfb3689e2 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -243,9 +243,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance.data['version'] = version['name'] - intent = context.data.get("intent") - if intent is not None: - anatomy_data["intent"] = intent + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + anatomy_data["intent"] = intent_value anatomy = instance.context.data['anatomy'] @@ -653,9 +656,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "fps": context.data.get( "fps", instance.data.get("fps"))} - intent = context.data.get("intent") - if intent is not None: - version_data["intent"] = intent + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") + + if intent_value: + version_data["intent"] = intent_value # Include optional data if present in optionals = [ diff --git a/pype/plugins/nuke/publish/extract_slate_frame.py b/pype/plugins/nuke/publish/extract_slate_frame.py index 369cbe0496..e1c05c3d1a 100644 --- a/pype/plugins/nuke/publish/extract_slate_frame.py +++ b/pype/plugins/nuke/publish/extract_slate_frame.py @@ -157,11 +157,13 @@ class ExtractSlateFrame(pype.api.Extractor): return comment = instance.context.data.get("comment") - intent = instance.context.data.get("intent", {}).get("value", "") + intent_value = instance.context.data.get("intent") + if intent_value and isinstance(intent_value, dict): + intent_value = intent_value.get("value") try: node["f_submission_note"].setValue(comment) - node["f_submitting_for"].setValue(intent) + node["f_submitting_for"].setValue(intent_value or "") except NameError: return instance.data.pop("slateNode") From f579e8467027c30da160be55365357c18c5993b2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Mar 2020 12:53:45 +0100 Subject: [PATCH 127/327] fix to subsetGroup --- pype/plugins/global/publish/submit_publish_job.py | 2 +- pype/scripts/otio_burnin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index e517198ba2..2914203578 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -366,7 +366,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = copy(instance_data) new_instance["subset"] = subset_name - new_instance["group"] = aov + new_instance["subsetGroup"] = aov ext = cols[0].tail.lstrip(".") diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 8d0b925089..8b52216968 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -296,7 +296,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): args=args, overwrite=overwrite ) - print(command) + # print(command) proc = subprocess.Popen(command, shell=True) proc.communicate() From f5426c78a61697e2381fb3c0c6438cefa69f75cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 19 Mar 2020 17:56:54 +0100 Subject: [PATCH 128/327] install custom excepthook to not crash blender --- pype/blender/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/blender/__init__.py b/pype/blender/__init__.py index 4b6074a820..4f52b4168a 100644 --- a/pype/blender/__init__.py +++ b/pype/blender/__init__.py @@ -1,4 +1,6 @@ import os +import sys +import traceback from avalon import api as avalon from pyblish import api as pyblish @@ -11,9 +13,16 @@ PUBLISH_PATH = os.path.join(PLUGINS_DIR, "blender", "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "blender", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "blender", "create") +ORIGINAL_EXCEPTHOOK = sys.excepthook + + +def pype_excepthook_handler(*args): + traceback.print_exception(*args) + def install(): """Install Blender configuration for Avalon.""" + sys.excepthook = pype_excepthook_handler pyblish.register_plugin_path(str(PUBLISH_PATH)) avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) @@ -21,6 +30,7 @@ def install(): def uninstall(): """Uninstall Blender configuration for Avalon.""" + sys.excepthook = ORIGINAL_EXCEPTHOOK pyblish.deregister_plugin_path(str(PUBLISH_PATH)) avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH)) From 05441eb2fe3df8b44e465118d6ef319ba774d535 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 18:07:36 +0100 Subject: [PATCH 129/327] fix imports --- pype/plugins/blender/create/create_action.py | 2 ++ .../blender/create/create_animation.py | 2 ++ pype/plugins/blender/create/create_model.py | 3 +- pype/plugins/blender/create/create_rig.py | 11 ++++--- pype/plugins/blender/load/load_action.py | 29 +++++++++-------- pype/plugins/blender/load/load_animation.py | 26 ++++++++-------- pype/plugins/blender/load/load_model.py | 25 +++++++-------- pype/plugins/blender/load/load_rig.py | 31 +++++++++---------- 8 files changed, 65 insertions(+), 64 deletions(-) diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 88ecebdfff..64dfe9ff90 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -4,6 +4,8 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin + class CreateAction(Creator): diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 14a50ba5ea..0758db280f 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -4,6 +4,8 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin + class CreateAnimation(Creator): diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index a3b2ffc55b..7a53f215f2 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -4,7 +4,7 @@ import bpy from avalon import api from avalon.blender import Creator, lib - +import pype.blender.plugin class CreateModel(Creator): """Polygonal static geometry""" @@ -15,7 +15,6 @@ class CreateModel(Creator): icon = "cube" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index f630c63966..b5860787ea 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -4,6 +4,7 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin class CreateRig(Creator): @@ -42,16 +43,16 @@ class CreateRig(Creator): self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) - # Add the rig object and all the children meshes to - # a set and link them all at the end to avoid duplicates. + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. # Blender crashes if trying to link an object that is already linked. - # This links automatically the children meshes if they were not + # This links automatically the children meshes if they were not # selected, and doesn't link them twice if they, insted, # were manually selected by the user. objects_to_link = set() if (self.options or {}).get("useSelection"): - + for obj in lib.get_selection(): objects_to_link.add( obj ) @@ -75,7 +76,7 @@ class CreateRig(Creator): # if len( custom_shapes ) > 0: # widgets_collection = bpy.data.collections.new(name="Widgets") - + # collection.children.link(widgets_collection) # for custom_shape in custom_shapes: diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 747bcd47f5..afde8b90a1 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -5,10 +5,9 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_action") @@ -50,7 +49,7 @@ class BlendActionLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -59,7 +58,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -89,16 +88,16 @@ class BlendActionLoader(pype.blender.AssetLoader): obj.animation_data.action.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + animation_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -153,7 +152,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -224,16 +223,16 @@ class BlendActionLoader(pype.blender.AssetLoader): strip.action = obj.animation_data.action strip.action_frame_end = obj.animation_data.action.frame_range[1] - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": collection.name}) objects_list.append(obj) - animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + animation_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -266,7 +265,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 0610517b67..ec3e24443f 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -5,15 +5,15 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin + logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") -class BlendAnimationLoader(pype.blender.AssetLoader): +class BlendAnimationLoader(pype.blender.plugin.AssetLoader): """Load animations from a .blend file. Warning: @@ -75,16 +75,16 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj.animation_data.action.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + animation_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -112,7 +112,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -121,7 +121,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -179,7 +179,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -239,12 +239,12 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] self._remove(self, objects, lib_container) - + bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 10904a1f7b..b8b6b9b956 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -5,15 +5,14 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendModelLoader(pype.blender.AssetLoader): +class BlendModelLoader(pype.blender.plugin.AssetLoader): """Load models from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -67,16 +66,16 @@ class BlendModelLoader(pype.blender.AssetLoader): material_slot.material.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + model_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -104,7 +103,7 @@ class BlendModelLoader(pype.blender.AssetLoader): collection = bpy.data.collections.new(lib_container) collection.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( collection, name, namespace, @@ -113,7 +112,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) container_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -169,7 +168,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -221,7 +220,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -232,7 +231,7 @@ class BlendModelLoader(pype.blender.AssetLoader): return True -class CacheModelLoader(pype.blender.AssetLoader): +class CacheModelLoader(pype.blender.plugin.AssetLoader): """Load cache models. Stores the imported asset in a collection named after the asset. diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index dcb70da6d8..44d47b41a1 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -5,15 +5,14 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendRigLoader(pype.blender.AssetLoader): +class BlendRigLoader(pype.blender.plugin.AssetLoader): """Load rigs from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -30,7 +29,7 @@ class BlendRigLoader(pype.blender.AssetLoader): label = "Link Rig" icon = "code-fork" color = "orange" - + @staticmethod def _remove(self, objects, lib_container): @@ -60,7 +59,7 @@ class BlendRigLoader(pype.blender.AssetLoader): meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - + objects_list = [] assert(len(armatures) == 1) @@ -74,11 +73,11 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.data.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) if obj.type == 'ARMATURE' and action is not None: @@ -86,8 +85,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action objects_list.append(obj) - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + + rig_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -115,7 +114,7 @@ class BlendRigLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -124,7 +123,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -182,7 +181,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -243,12 +242,12 @@ class BlendRigLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] self._remove(self, objects, lib_container) - + bpy.data.collections.remove(collection) return True From a6ec2060ccff6117cf39ab5768094a4109e4cb1d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 19:29:54 +0100 Subject: [PATCH 130/327] filter master version to only some families --- pype/plugins/global/publish/integrate_master_version.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 0eba275407..16aa0dd23d 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -15,6 +15,15 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # Must happen after IntegrateNew order = pyblish.api.IntegratorOrder + 0.1 + optional = True + + families = ["model", + "rig", + "setdress", + "look", + "pointcache", + "animation"] + # Can specify representation names that will be ignored (lower case) ignored_representation_names = [] db_representation_context_keys = [ From 5ff73c064ecae24e0470529503bda8ab76875ddd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 19:40:40 +0100 Subject: [PATCH 131/327] remove extra imports --- pype/plugins/blender/create/create_action.py | 8 ++++---- pype/plugins/blender/create/create_animation.py | 2 -- pype/plugins/blender/create/create_model.py | 1 + pype/plugins/blender/create/create_rig.py | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 64dfe9ff90..68e2a50b61 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -7,7 +7,6 @@ from avalon.blender import Creator, lib import pype.blender.plugin - class CreateAction(Creator): """Action output for character rigs""" @@ -17,7 +16,6 @@ class CreateAction(Creator): icon = "male" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] @@ -29,9 +27,11 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if obj.animation_data is not None and obj.animation_data.action is not None: + if (obj.animation_data is not None + and obj.animation_data.action is not None): - empty_obj = bpy.data.objects.new( name = name, object_data = None ) + empty_obj = bpy.data.objects.new(name=name, + object_data=None) empty_obj.animation_data_create() empty_obj.animation_data.action = obj.animation_data.action empty_obj.animation_data.action.name = name diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 0758db280f..b40a456c8f 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -7,7 +7,6 @@ from avalon.blender import Creator, lib import pype.blender.plugin - class CreateAnimation(Creator): """Animation output for character rigs""" @@ -17,7 +16,6 @@ class CreateAnimation(Creator): icon = "male" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 7a53f215f2..303a7a63a1 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -6,6 +6,7 @@ from avalon import api from avalon.blender import Creator, lib import pype.blender.plugin + class CreateModel(Creator): """Polygonal static geometry""" diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index b5860787ea..d28e854232 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -33,7 +33,6 @@ class CreateRig(Creator): # return found def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] @@ -61,7 +60,7 @@ class CreateRig(Creator): for subobj in obj.children: - objects_to_link.add( subobj ) + objects_to_link.add(subobj) # Create a new collection and link the widgets that # the rig uses. From fe8a71f97a271a6a07079c1947130f3fee207b18 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Mar 2020 21:16:40 +0100 Subject: [PATCH 132/327] grouping by layer name --- pype/plugins/global/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 2914203578..9c556f3512 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -366,7 +366,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = copy(instance_data) new_instance["subset"] = subset_name - new_instance["subsetGroup"] = aov + new_instance["subsetGroup"] = subset ext = cols[0].tail.lstrip(".") From 81ccb8d767f9eefeb5dc00eea9874330f0113976 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 20 Mar 2020 08:59:09 +0100 Subject: [PATCH 133/327] stop hound from reporting line break before operator --- .flake8 | 1 + 1 file changed, 1 insertion(+) diff --git a/.flake8 b/.flake8 index 67ed2d77a3..f28d8cbfc3 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,7 @@ [flake8] # ignore = D203 ignore = BLK100 +ignore = W504 max-line-length = 79 exclude = .git, From e8ac6ddbf9b89558601eee7fbc17533518c20b82 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Mar 2020 11:32:21 +0100 Subject: [PATCH 134/327] fixed group name to include full subset name but '_AOV' --- pype/plugins/global/publish/submit_publish_job.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 9c556f3512..556132cd77 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -348,10 +348,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): assert len(cols) == 1, "only one image sequence type is expected" # create subset name `familyTaskSubset_AOV` - subset_name = 'render{}{}{}{}_{}'.format( + group_name = 'render{}{}{}{}'.format( task[0].upper(), task[1:], - subset[0].upper(), subset[1:], - aov) + subset[0].upper(), subset[1:]) + + subset_name = '{}_{}'.format(group_name, aov) staging = os.path.dirname(list(cols[0])[0]) @@ -366,7 +367,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = copy(instance_data) new_instance["subset"] = subset_name - new_instance["subsetGroup"] = subset + new_instance["subsetGroup"] = group_name ext = cols[0].tail.lstrip(".") From ac178e9d5b5336d607fa37c0755329e4977c224e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 20 Mar 2020 12:05:10 +0100 Subject: [PATCH 135/327] use right intent variable in integrate new --- pype/plugins/global/publish/integrate_new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index ccfb3689e2..5052ae3aff 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -263,8 +263,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): for idx, repre in enumerate(instance.data["representations"]): # create template data for Anatomy template_data = copy.deepcopy(anatomy_data) - if intent is not None: - template_data["intent"] = intent + if intent_value is not None: + template_data["intent"] = intent_value resolution_width = repre.get("resolutionWidth") resolution_height = repre.get("resolutionHeight") From 0f8b35c831420dc55ff07699ab54f1064b9a3004 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 11:31:59 +0000 Subject: [PATCH 136/327] Fixed Loader's parent class --- pype/plugins/blender/load/load_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index afde8b90a1..e185bff7a8 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -12,7 +12,7 @@ import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_action") -class BlendActionLoader(pype.blender.AssetLoader): +class BlendActionLoader(pype.blender.plugin.AssetLoader): """Load action from a .blend file. Warning: From 7e8bb47ed727a5798427e393dac2361ba555b065 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 12:29:34 +0000 Subject: [PATCH 137/327] Fixed creation of the animation and fbx publishing --- .../blender/create/create_animation.py | 22 +++++++++++- .../blender/publish/extract_fbx_animation.py | 36 +++++++++++-------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index b40a456c8f..6b7616bbfd 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -25,8 +25,28 @@ class CreateAnimation(Creator): self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. + # Blender crashes if trying to link an object that is already linked. + # This links automatically the children meshes if they were not + # selected, and doesn't link them twice if they, insted, + # were manually selected by the user. + objects_to_link = set() + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): - collection.objects.link(obj) + + objects_to_link.add( obj ) + + if obj.type == 'ARMATURE': + + for subobj in obj.children: + + objects_to_link.add(subobj) + + for obj in objects_to_link: + + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py index bc088f8bb7..4b1fe98c2f 100644 --- a/pype/plugins/blender/publish/extract_fbx_animation.py +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -47,8 +47,7 @@ class ExtractAnimationFBX(pype.api.Extractor): # We set the scale of the scene for the export bpy.context.scene.unit_settings.scale_length = 0.01 - # We export all the objects in the collection - objects_to_export = collections[0].objects + armatures = [obj for obj in collections[0].objects if obj.type == 'ARMATURE'] object_action_pairs = [] original_actions = [] @@ -56,20 +55,25 @@ class ExtractAnimationFBX(pype.api.Extractor): starting_frames = [] ending_frames = [] - # For each object, we make a copy of the current action - for obj in objects_to_export: + # For each armature, we make a copy of the current action + for obj in armatures: - curr_action = obj.animation_data.action - copy_action = curr_action.copy() + curr_action = None + copy_action = None + + if obj.animation_data and obj.animation_data.action: + + curr_action = obj.animation_data.action + copy_action = curr_action.copy() + + curr_frame_range = curr_action.frame_range + + starting_frames.append( curr_frame_range[0] ) + ending_frames.append( curr_frame_range[1] ) object_action_pairs.append((obj, copy_action)) original_actions.append(curr_action) - curr_frame_range = curr_action.frame_range - - starting_frames.append( curr_frame_range[0] ) - ending_frames.append( curr_frame_range[1] ) - # We compute the starting and ending frames max_frame = min( starting_frames ) min_frame = max( ending_frames ) @@ -98,10 +102,14 @@ class ExtractAnimationFBX(pype.api.Extractor): # We delete the baked action and set the original one back for i in range(0, len(object_action_pairs)): - object_action_pairs[i][0].animation_data.action = original_actions[i] + if original_actions[i]: - object_action_pairs[i][1].user_clear() - bpy.data.actions.remove(object_action_pairs[i][1]) + object_action_pairs[i][0].animation_data.action = original_actions[i] + + if object_action_pairs[i][1]: + + object_action_pairs[i][1].user_clear() + bpy.data.actions.remove(object_action_pairs[i][1]) if "representations" not in instance.data: instance.data["representations"] = [] From a3d025f7845ad3ef30f04fe5b7fcb7f6b0c8ab20 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 16:13:55 +0000 Subject: [PATCH 138/327] PEP8 compliance --- pype/blender/plugin.py | 16 +++--- pype/plugins/blender/create/create_action.py | 4 +- .../blender/create/create_animation.py | 2 +- pype/plugins/blender/create/create_rig.py | 43 +------------- pype/plugins/blender/load/load_action.py | 40 ++++++++----- pype/plugins/blender/load/load_animation.py | 24 +++++--- pype/plugins/blender/load/load_model.py | 8 ++- pype/plugins/blender/load/load_rig.py | 11 ++-- .../plugins/blender/publish/collect_action.py | 6 +- .../blender/publish/collect_animation.py | 10 ++-- .../blender/publish/collect_current_file.py | 3 +- pype/plugins/blender/publish/collect_model.py | 6 +- pype/plugins/blender/publish/collect_rig.py | 6 +- pype/plugins/blender/publish/extract_blend.py | 3 +- pype/plugins/blender/publish/extract_fbx.py | 34 +++++++---- .../blender/publish/extract_fbx_animation.py | 57 ++++++++++++------- 16 files changed, 137 insertions(+), 136 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index b441714c0d..5e98d8314b 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -10,14 +10,16 @@ from avalon import api VALID_EXTENSIONS = [".blend"] -def asset_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: +def asset_name( + asset: str, subset: str, namespace: Optional[str] = None +) -> str: """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: name = f"{namespace}:{name}" return name -def create_blender_context( obj: Optional[bpy.types.Object] = None ): +def create_blender_context(obj: Optional[bpy.types.Object] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ @@ -27,16 +29,16 @@ def create_blender_context( obj: Optional[bpy.types.Object] = None ): for region in area.regions: if region.type == 'WINDOW': override_context = { - 'window': win, - 'screen': win.screen, - 'area': area, - 'region': region, + 'window': win, + 'screen': win.screen, + 'area': area, + 'region': region, 'scene': bpy.context.scene, 'active_object': obj, 'selected_objects': [obj] } return override_context - raise Exception( "Could not create a custom Blender context." ) + raise Exception("Could not create a custom Blender context.") class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 68e2a50b61..6c24065f81 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -27,8 +27,8 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if (obj.animation_data is not None - and obj.animation_data.action is not None): + if (obj.animation_data is not None and + obj.animation_data.action is not None): empty_obj = bpy.data.objects.new(name=name, object_data=None) diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 6b7616bbfd..3a5985d7a2 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -37,7 +37,7 @@ class CreateAnimation(Creator): for obj in lib.get_selection(): - objects_to_link.add( obj ) + objects_to_link.add(obj) if obj.type == 'ARMATURE': diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index d28e854232..dc97d8b4ce 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -15,23 +15,6 @@ class CreateRig(Creator): family = "rig" icon = "wheelchair" - # @staticmethod - # def _find_layer_collection(self, layer_collection, collection): - - # found = None - - # if (layer_collection.collection == collection): - - # return layer_collection - - # for layer in layer_collection.children: - - # found = self._find_layer_collection(layer, collection) - - # if found: - - # return found - def process(self): asset = self.data["asset"] @@ -54,7 +37,7 @@ class CreateRig(Creator): for obj in lib.get_selection(): - objects_to_link.add( obj ) + objects_to_link.add(obj) if obj.type == 'ARMATURE': @@ -62,30 +45,6 @@ class CreateRig(Creator): objects_to_link.add(subobj) - # Create a new collection and link the widgets that - # the rig uses. - # custom_shapes = set() - - # for posebone in obj.pose.bones: - - # if posebone.custom_shape is not None: - - # custom_shapes.add( posebone.custom_shape ) - - # if len( custom_shapes ) > 0: - - # widgets_collection = bpy.data.collections.new(name="Widgets") - - # collection.children.link(widgets_collection) - - # for custom_shape in custom_shapes: - - # widgets_collection.objects.link( custom_shape ) - - # layer_collection = self._find_layer_collection(bpy.context.view_layer.layer_collection, widgets_collection) - - # layer_collection.exclude = True - for obj in objects_to_link: collection.objects.link(obj) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index e185bff7a8..303d1ead4d 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -69,11 +69,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + collection = bpy.context.scene.collection - scene.collection.children.link(bpy.data.collections[lib_container]) + collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + animation_container = collection.children[lib_container].make_local() objects_list = [] @@ -84,9 +84,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): obj = obj.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -173,8 +175,12 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): strips = [] for obj in collection_metadata["objects"]: + + # Get all the strips that use the action + arm_objs = [ + arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] - for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + for armature_obj in arm_objs: if armature_obj.animation_data is not None: @@ -203,25 +209,27 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + anim_container = scene.collection.children[lib_container].make_local() objects_list = [] # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in animation_container.objects: + for obj in anim_container.objects: obj = obj.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() for strip in strips: - strip.action = obj.animation_data.action - strip.action_frame_end = obj.animation_data.action.frame_range[1] + strip.action = anim_data.action + strip.action_frame_end = anim_data.action.frame_range[1] if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -232,7 +240,7 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - animation_container.pop(blender.pipeline.AVALON_PROPERTY) + anim_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -271,7 +279,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): for obj in objects: - for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + # Get all the strips that use the action + arm_objs = [ + arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] + + for armature_obj in arm_objs: if armature_obj.animation_data is not None: diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index ec3e24443f..395684a3ba 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -10,7 +10,8 @@ import bpy import pype.blender.plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") +logger = logging.getLogger("pype").getChild( + "blender").getChild("load_animation") class BlendAnimationLoader(pype.blender.plugin.AssetLoader): @@ -53,10 +54,11 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + anim_container = scene.collection.children[lib_container].make_local() - meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + meshes = [obj for obj in anim_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in anim_container.objects if obj.type == 'ARMATURE'] # Should check if there is only an armature? @@ -71,9 +73,11 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): obj.data.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -84,7 +88,7 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - animation_container.pop( blender.pipeline.AVALON_PROPERTY ) + anim_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -126,7 +130,8 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name) + objects_list = self._process( + self, libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -206,7 +211,8 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name) + objects_list = self._process( + self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index b8b6b9b956..ff7c6c49c2 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -75,7 +75,7 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - model_container.pop( blender.pipeline.AVALON_PROPERTY ) + model_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -117,7 +117,8 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name) + objects_list = self._process( + self, libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -190,7 +191,8 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name) + objects_list = self._process( + self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 44d47b41a1..d14a868722 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -58,7 +58,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): rig_container = scene.collection.children[lib_container].make_local() meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] @@ -86,7 +87,7 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - rig_container.pop( blender.pipeline.AVALON_PROPERTY ) + rig_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -128,7 +129,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name, None) + objects_list = self._process( + self, libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -209,7 +211,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name, action) + objects_list = self._process( + self, str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index 9a54045cea..a8ceed9c82 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectAction(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'action' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 109ae98e6f..50d49692b8 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -20,13 +18,13 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded animation - and we don't want to publish it. + representation set. If the representation is set, it is a loaded + animation and we don't want to publish it. """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'animation' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_current_file.py b/pype/plugins/blender/publish/collect_current_file.py index 926d290b31..72976c490b 100644 --- a/pype/plugins/blender/publish/collect_current_file.py +++ b/pype/plugins/blender/publish/collect_current_file.py @@ -15,4 +15,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): current_file = bpy.data.filepath context.data['currentFile'] = current_file - assert current_file != '', "Current file is empty. Save the file before continuing." + assert current_file != '', "Current file is empty. " \ + "Save the file before continuing." diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_model.py index ee10eaf7f2..df5c1e709a 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_model.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'model' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py index a4b30541f6..01958da37a 100644 --- a/pype/plugins/blender/publish/collect_rig.py +++ b/pype/plugins/blender/publish/collect_rig.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectRig(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'rig' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 032f85897d..5f3fdac293 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -43,4 +43,5 @@ class ExtractBlend(pype.api.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_fbx.py b/pype/plugins/blender/publish/extract_fbx.py index 95466c1d2b..231bfdde24 100644 --- a/pype/plugins/blender/publish/extract_fbx.py +++ b/pype/plugins/blender/publish/extract_fbx.py @@ -1,10 +1,10 @@ import os -import avalon.blender.workio import pype.api import bpy + class ExtractFBX(pype.api.Extractor): """Extract as FBX.""" @@ -20,29 +20,39 @@ class ExtractFBX(pype.api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) + context = bpy.context + scene = context.scene + view_layer = context.view_layer + # Perform extraction self.log.info("Performing extraction..") - collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] - assert len(collections) == 1, "There should be one and only one collection collected for this asset" + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" - old_active_layer_collection = bpy.context.view_layer.active_layer_collection + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children # Get the layer collection from the collection we need to export. - # This is needed because in Blender you can only set the active + # This is needed because in Blender you can only set the active # collection with the layer collection, and there is no way to get - # the layer collection from the collection (but there is the vice versa). - layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] assert len(layer_collections) == 1 - bpy.context.view_layer.active_layer_collection = layer_collections[0] + view_layer.active_layer_collection = layer_collections[0] - old_scale = bpy.context.scene.unit_settings.scale_length + old_scale = scene.unit_settings.scale_length # We set the scale of the scene for the export - bpy.context.scene.unit_settings.scale_length = 0.01 + scene.unit_settings.scale_length = 0.01 # We export the fbx bpy.ops.export_scene.fbx( @@ -52,9 +62,9 @@ class ExtractFBX(pype.api.Extractor): add_leaf_bones=False ) - bpy.context.view_layer.active_layer_collection = old_active_layer_collection + view_layer.active_layer_collection = old_active_layer_collection - bpy.context.scene.unit_settings.scale_length = old_scale + scene.unit_settings.scale_length = old_scale if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py index 4b1fe98c2f..d51c641e9c 100644 --- a/pype/plugins/blender/publish/extract_fbx_animation.py +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -1,5 +1,4 @@ import os -import avalon.blender.workio import pype.api @@ -23,31 +22,42 @@ class ExtractAnimationFBX(pype.api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) + context = bpy.context + scene = context.scene + view_layer = context.view_layer + # Perform extraction self.log.info("Performing extraction..") - collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] - assert len(collections) == 1, "There should be one and only one collection collected for this asset" + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" - old_active_layer_collection = bpy.context.view_layer.active_layer_collection + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children # Get the layer collection from the collection we need to export. - # This is needed because in Blender you can only set the active + # This is needed because in Blender you can only set the active # collection with the layer collection, and there is no way to get - # the layer collection from the collection (but there is the vice versa). - layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] assert len(layer_collections) == 1 - bpy.context.view_layer.active_layer_collection = layer_collections[0] + view_layer.active_layer_collection = layer_collections[0] - old_scale = bpy.context.scene.unit_settings.scale_length + old_scale = scene.unit_settings.scale_length # We set the scale of the scene for the export - bpy.context.scene.unit_settings.scale_length = 0.01 + scene.unit_settings.scale_length = 0.01 - armatures = [obj for obj in collections[0].objects if obj.type == 'ARMATURE'] + armatures = [ + obj for obj in collections[0].objects if obj.type == 'ARMATURE'] object_action_pairs = [] original_actions = [] @@ -68,15 +78,15 @@ class ExtractAnimationFBX(pype.api.Extractor): curr_frame_range = curr_action.frame_range - starting_frames.append( curr_frame_range[0] ) - ending_frames.append( curr_frame_range[1] ) + starting_frames.append(curr_frame_range[0]) + ending_frames.append(curr_frame_range[1]) object_action_pairs.append((obj, copy_action)) original_actions.append(curr_action) # We compute the starting and ending frames - max_frame = min( starting_frames ) - min_frame = max( ending_frames ) + max_frame = min(starting_frames) + min_frame = max(ending_frames) # We bake the copy of the current action for each object bpy_extras.anim_utils.bake_action_objects( @@ -95,21 +105,24 @@ class ExtractAnimationFBX(pype.api.Extractor): add_leaf_bones=False ) - bpy.context.view_layer.active_layer_collection = old_active_layer_collection + view_layer.active_layer_collection = old_active_layer_collection - bpy.context.scene.unit_settings.scale_length = old_scale + scene.unit_settings.scale_length = old_scale # We delete the baked action and set the original one back for i in range(0, len(object_action_pairs)): - if original_actions[i]: + pair = object_action_pairs[i] + action = original_actions[i] - object_action_pairs[i][0].animation_data.action = original_actions[i] + if action: - if object_action_pairs[i][1]: + pair[0].animation_data.action = action - object_action_pairs[i][1].user_clear() - bpy.data.actions.remove(object_action_pairs[i][1]) + if pair[1]: + + pair[1].user_clear() + bpy.data.actions.remove(pair[1]) if "representations" not in instance.data: instance.data["representations"] = [] From 40cf778f106c32deec2d850d0c72c906537ebfcf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 16:46:45 +0000 Subject: [PATCH 139/327] More PEP8 compliance --- pype/blender/plugin.py | 5 ++++- pype/plugins/blender/create/create_action.py | 4 ++-- pype/plugins/blender/load/load_action.py | 2 +- pype/plugins/blender/publish/collect_action.py | 4 ++-- pype/plugins/blender/publish/collect_animation.py | 6 +++--- pype/plugins/blender/publish/collect_model.py | 4 ++-- pype/plugins/blender/publish/collect_rig.py | 4 ++-- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index 5e98d8314b..8f72d04a1d 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -19,6 +19,7 @@ def asset_name( name = f"{namespace}:{name}" return name + def create_blender_context(obj: Optional[bpy.types.Object] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. @@ -40,6 +41,7 @@ def create_blender_context(obj: Optional[bpy.types.Object] = None): return override_context raise Exception("Could not create a custom Blender context.") + class AssetLoader(api.Loader): """A basic AssetLoader for Blender @@ -89,7 +91,8 @@ class AssetLoader(api.Loader): assert obj.library, f"'{obj.name}' is not linked." libraries.add(obj.library) - assert len(libraries) == 1, "'{container.name}' contains objects from more then 1 library." + assert len( + libraries) == 1, "'{container.name}' contains objects from more then 1 library." return list(libraries)[0] diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 6c24065f81..68e2a50b61 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -27,8 +27,8 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if (obj.animation_data is not None and - obj.animation_data.action is not None): + if (obj.animation_data is not None + and obj.animation_data.action is not None): empty_obj = bpy.data.objects.new(name=name, object_data=None) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 303d1ead4d..a1b1ad3cea 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -175,7 +175,7 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): strips = [] for obj in collection_metadata["objects"]: - + # Get all the strips that use the action arm_objs = [ arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index a8ceed9c82..c359198490 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -23,8 +23,8 @@ class CollectAction(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'action' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 50d49692b8..681f945f25 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -18,13 +18,13 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded + representation set. If the representation is set, it is a loaded animation and we don't want to publish it. """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'animation' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_model.py index df5c1e709a..5cbd097a4e 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_model.py @@ -23,8 +23,8 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'model' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py index 01958da37a..730f209e89 100644 --- a/pype/plugins/blender/publish/collect_rig.py +++ b/pype/plugins/blender/publish/collect_rig.py @@ -23,8 +23,8 @@ class CollectRig(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'rig' + and not avalon_prop.get('representation')): yield collection def process(self, context): From 31f0ab63788037a11c1fecb7817bb621651159ad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 Mar 2020 21:35:23 +0100 Subject: [PATCH 140/327] feat(nuke): prerender node dont need `review` knob --- pype/nuke/lib.py | 38 +++++++++++++------ .../nuke/create/create_write_prerender.py | 4 +- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 8e241dad16..989cbf569f 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -215,14 +215,14 @@ def script_name(): def add_button_write_to_read(node): name = "createReadNode" - label = "Create Read" + label = "[ Create Read ]" value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())" k = nuke.PyScript_Knob(name, label, value) k.setFlag(0x1000) node.addKnob(k) -def create_write_node(name, data, input=None, prenodes=None): +def create_write_node(name, data, input=None, prenodes=None, review=True): ''' Creating write node which is group node Arguments: @@ -231,6 +231,7 @@ def create_write_node(name, data, input=None, prenodes=None): input (node): selected node to connect to prenodes (list, optional): list of lists, definitions for nodes to be created before write + review (bool): adding review knob Example: prenodes = [( @@ -380,15 +381,8 @@ def create_write_node(name, data, input=None, prenodes=None): add_rendering_knobs(GN) - # adding write to read button - add_button_write_to_read(GN) - - divider = nuke.Text_Knob('') - GN.addKnob(divider) - - # set tile color - tile_color = _data.get("tile_color", "0xff0000ff") - GN["tile_color"].setValue(tile_color) + if review: + add_review_knob(GN) # add render button lnk = nuke.Link_Knob("Render") @@ -396,9 +390,20 @@ def create_write_node(name, data, input=None, prenodes=None): lnk.setName("Render") GN.addKnob(lnk) + divider = nuke.Text_Knob('') + GN.addKnob(divider) + + # adding write to read button + add_button_write_to_read(GN) + # Deadline tab. add_deadline_tab(GN) + + # set tile color + tile_color = _data.get("tile_color", "0xff0000ff") + GN["tile_color"].setValue(tile_color) + return GN @@ -420,6 +425,17 @@ def add_rendering_knobs(node): knob = nuke.Boolean_Knob("render_farm", "Render on Farm") knob.setValue(False) node.addKnob(knob) + return node + +def add_review_knob(node): + ''' Adds additional review knob to given node + + Arguments: + node (obj): nuke node object to be fixed + + Return: + node (obj): with added knob + ''' if "review" not in node.knobs(): knob = nuke.Boolean_Knob("review", "Review") knob.setValue(True) diff --git a/pype/plugins/nuke/create/create_write_prerender.py b/pype/plugins/nuke/create/create_write_prerender.py index f8210db9db..6e242f886c 100644 --- a/pype/plugins/nuke/create/create_write_prerender.py +++ b/pype/plugins/nuke/create/create_write_prerender.py @@ -87,7 +87,9 @@ class CreateWritePrerender(plugin.PypeCreator): self.data["subset"], write_data, input=selected_node, - prenodes=[]) + prenodes=[], + review=False + ) # relinking to collected connections for i, input in enumerate(inputs): From 5c7bba0c0676a95a35b1de2e03c523989d7197b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 Mar 2020 21:36:13 +0100 Subject: [PATCH 141/327] feat(nuke): setting up properly families --- pype/plugins/nuke/publish/collect_review.py | 2 +- pype/plugins/nuke/publish/collect_slate_node.py | 2 +- pype/plugins/nuke/publish/extract_render_local.py | 2 +- pype/plugins/nuke/publish/submit_nuke_deadline.py | 2 +- pype/plugins/nuke/publish/validate_write_bounding_box.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_review.py b/pype/plugins/nuke/publish/collect_review.py index f02f22e053..c95c94541d 100644 --- a/pype/plugins/nuke/publish/collect_review.py +++ b/pype/plugins/nuke/publish/collect_review.py @@ -9,7 +9,7 @@ class CollectReview(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.3 label = "Collect Review" hosts = ["nuke"] - families = ["write", "prerender"] + families = ["render", "render.local", "render.farm"] def process(self, instance): diff --git a/pype/plugins/nuke/publish/collect_slate_node.py b/pype/plugins/nuke/publish/collect_slate_node.py index d8d6b50f05..9c7f1b5e95 100644 --- a/pype/plugins/nuke/publish/collect_slate_node.py +++ b/pype/plugins/nuke/publish/collect_slate_node.py @@ -8,7 +8,7 @@ class CollectSlate(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.09 label = "Collect Slate Node" hosts = ["nuke"] - families = ["write"] + families = ["render", "render.local", "render.farm"] def process(self, instance): node = instance[0] diff --git a/pype/plugins/nuke/publish/extract_render_local.py b/pype/plugins/nuke/publish/extract_render_local.py index 5467d239c2..1dad413ee5 100644 --- a/pype/plugins/nuke/publish/extract_render_local.py +++ b/pype/plugins/nuke/publish/extract_render_local.py @@ -17,7 +17,7 @@ class NukeRenderLocal(pype.api.Extractor): order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["nuke"] - families = ["render.local"] + families = ["render.local", "prerender.local"] def process(self, instance): node = None diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 0a9ef33398..3da2e58e4d 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -19,7 +19,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): label = "Submit to Deadline" order = pyblish.api.IntegratorOrder + 0.1 hosts = ["nuke", "nukestudio"] - families = ["render.farm"] + families = ["render.farm", "prerender.farm"] optional = True deadline_priority = 50 diff --git a/pype/plugins/nuke/publish/validate_write_bounding_box.py b/pype/plugins/nuke/publish/validate_write_bounding_box.py index cedeea6d9f..e4b7c77a25 100644 --- a/pype/plugins/nuke/publish/validate_write_bounding_box.py +++ b/pype/plugins/nuke/publish/validate_write_bounding_box.py @@ -57,7 +57,7 @@ class ValidateNukeWriteBoundingBox(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder optional = True - families = ["write"] + families = ["render", "render.local", "render.farm"] label = "Write Bounding Box" hosts = ["nuke"] actions = [RepairNukeBoundingBoxAction] From 63eac17649a7c42b4ba4f8d93c06f0dfad1af346 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 Mar 2020 21:36:46 +0100 Subject: [PATCH 142/327] clean(nuke): old code and better definition of families --- pype/plugins/nuke/publish/collect_writes.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index aa5f825a98..f3f33b7a6d 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -105,8 +105,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): representation['files'] = collected_frames instance.data["representations"].append(representation) - if "render" not in instance.data["families"]: - instance.data["families"].append("render") except Exception: instance.data["representations"].append(representation) self.log.debug("couldn't collect frames: {}".format(label)) @@ -127,6 +125,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): deadlinePriority = group_node["deadlinePriority"].value() families = [f for f in instance.data["families"] if "write" not in f] + instance.data.update({ "versionData": version_data, "path": path, @@ -147,18 +146,5 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "deadlinePriority": deadlinePriority }) self.log.debug("families: {}".format(families)) - if "prerender" in families: - _families = list() - for fm in families: - if fm in _families: - continue - if "render" in fm: - if "prerender" in fm: - continue - _families.append(fm) - instance.data.update({ - "family": "prerender", - "families": _families - }) - self.log.debug("_families: {}".format(_families)) + self.log.debug("instance.data: {}".format(instance.data)) From 7fbb72b8a6eecdaf0d3799b59a248aa6cded0aa3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 Mar 2020 21:37:10 +0100 Subject: [PATCH 143/327] clean(nuke): improving code --- .../plugins/nuke/publish/collect_instances.py | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 893c6db7e5..54891d189c 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -52,6 +52,7 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): # establish families family = avalon_knob_data["family"] + families_ak = avalon_knob_data.get("families") families = list() # except disabled nodes but exclude backdrops in test @@ -68,20 +69,16 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): # Add all nodes in group instances. if node.Class() == "Group": # only alter families for render family - if ("render" in family): - # check if node is not disabled - families.append(avalon_knob_data["families"]) + if "write" in families_ak: if node["render"].value(): self.log.info("flagged for render") - add_family = "render.local" + add_family = "{}.local".format(family) # dealing with local/farm rendering if node["render_farm"].value(): self.log.info("adding render farm family") - add_family = "render.farm" + add_family = "{}.farm".format(family) instance.data["transfer"] = False families.append(add_family) - if "prerender" in family: - families.append("prerender") else: # add family into families families.insert(0, family) @@ -91,7 +88,7 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): instance.append(i) node.end() - families_ak = avalon_knob_data.get("families") + self.log.debug("__ families: `{}`".format(families)) if families_ak: families.append(families_ak) @@ -104,22 +101,6 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): resolution_height = format.height() pixel_aspect = format.pixelAspect() - if node.Class() not in "Read": - if "render" not in node.knobs().keys(): - pass - elif node["render"].value(): - self.log.info("flagged for render") - add_family = "render.local" - # dealing with local/farm rendering - if node["render_farm"].value(): - self.log.info("adding render farm family") - add_family = "render.farm" - instance.data["transfer"] = False - families.append(add_family) - else: - # add family into families - families.insert(0, family) - instance.data.update({ "subset": subset, "asset": os.environ["AVALON_ASSET"], From b3702c49df14b2351cfc9c99e8987fb4e5685896 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 Mar 2020 21:37:37 +0100 Subject: [PATCH 144/327] feat(global): explicit families for integrate new --- pype/plugins/global/publish/integrate_new.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index aa214f36cb..ddb40e321a 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -64,6 +64,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "scene", "vrayproxy", "render", + "render.local", + "prerender", + "prerender.local", "imagesequence", "review", "rendersetup", From 9a1167c26d784a43bb6643b4fcd6514d2aebe7b0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 20 Mar 2020 21:38:11 +0100 Subject: [PATCH 145/327] feat(nuke): dealing with `prerender` family when submitting to deadline --- pype/plugins/global/publish/submit_publish_job.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index dcf19ae32c..0b7a8473d4 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -141,7 +141,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): hosts = ["fusion", "maya", "nuke"] - families = ["render.farm", "renderlayer", "imagesequence"] + families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence"] aov_filter = {"maya": ["beauty"]} @@ -583,6 +583,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionHeight": data.get("resolutionHeight", 1080), } + if "prerender.farm" in instance.data["families"]: + instance_skeleton_data.update({ + "family": "prerender", + "families": ["prerender"] + }) + # transfer specific families from original instance to new render for item in self.families_transfer: if item in instance.data.get("families", []): From 043d8225a368510acb3427a92ae2672e23501367 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:02 +0100 Subject: [PATCH 146/327] unify collectors --- .../plugins/blender/publish/collect_action.py | 51 ------------------- .../blender/publish/collect_animation.py | 51 ------------------- ...{collect_model.py => collect_instances.py} | 17 ++++--- pype/plugins/blender/publish/collect_rig.py | 51 ------------------- 4 files changed, 10 insertions(+), 160 deletions(-) delete mode 100644 pype/plugins/blender/publish/collect_action.py delete mode 100644 pype/plugins/blender/publish/collect_animation.py rename pype/plugins/blender/publish/{collect_model.py => collect_instances.py} (78%) delete mode 100644 pype/plugins/blender/publish/collect_rig.py diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py deleted file mode 100644 index c359198490..0000000000 --- a/pype/plugins/blender/publish/collect_action.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectAction(pyblish.api.ContextPlugin): - """Collect the data of an action.""" - - hosts = ["blender"] - label = "Collect Action" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_action_collections() -> Generator: - """Return all 'animation' collections. - - Check if the family is 'action' and if it doesn't have the - representation set. If the representation is set, it is a loaded action - and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the actions from the current Blender scene.""" - collections = self.get_action_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py deleted file mode 100644 index 681f945f25..0000000000 --- a/pype/plugins/blender/publish/collect_animation.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectAnimation(pyblish.api.ContextPlugin): - """Collect the data of an animation.""" - - hosts = ["blender"] - label = "Collect Animation" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_animation_collections() -> Generator: - """Return all 'animation' collections. - - Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded - animation and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the animations from the current Blender scene.""" - collections = self.get_animation_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_instances.py similarity index 78% rename from pype/plugins/blender/publish/collect_model.py rename to pype/plugins/blender/publish/collect_instances.py index 5cbd097a4e..1d3693216d 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_instances.py @@ -1,20 +1,21 @@ from typing import Generator import bpy +import json import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY -class CollectModel(pyblish.api.ContextPlugin): +class CollectInstances(pyblish.api.ContextPlugin): """Collect the data of a model.""" hosts = ["blender"] - label = "Collect Model" + label = "Collect Instances" order = pyblish.api.CollectorOrder @staticmethod - def get_model_collections() -> Generator: + def get_collections() -> Generator: """Return all 'model' collections. Check if the family is 'model' and if it doesn't have the @@ -23,13 +24,13 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' - and not avalon_prop.get('representation')): + if avalon_prop.get('id') == 'pyblish.avalon.instance': yield collection def process(self, context): """Collect the models from the current Blender scene.""" - collections = self.get_model_collections() + collections = self.get_collections() + for collection in collections: avalon_prop = collection[AVALON_PROPERTY] asset = avalon_prop['asset'] @@ -48,4 +49,6 @@ class CollectModel(pyblish.api.ContextPlugin): members = list(collection.objects) members.append(collection) instance[:] = members - self.log.debug(instance.data) + self.log.debug(json.dumps(instance.data, indent=4)) + for obj in instance: + self.log.debug(obj) diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py deleted file mode 100644 index 730f209e89..0000000000 --- a/pype/plugins/blender/publish/collect_rig.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectRig(pyblish.api.ContextPlugin): - """Collect the data of a rig.""" - - hosts = ["blender"] - label = "Collect Rig" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_rig_collections() -> Generator: - """Return all 'rig' collections. - - Check if the family is 'rig' and if it doesn't have the - representation set. If the representation is set, it is a loaded rig - and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the rigs from the current Blender scene.""" - collections = self.get_rig_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) From 00bec457481636d48be8ec516786b83594061a82 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:34 +0100 Subject: [PATCH 147/327] naive fix to crashing uv validator --- .../blender/publish/validate_mesh_has_uv.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/plugins/blender/publish/validate_mesh_has_uv.py b/pype/plugins/blender/publish/validate_mesh_has_uv.py index b71a40ad8f..d0cd33645b 100644 --- a/pype/plugins/blender/publish/validate_mesh_has_uv.py +++ b/pype/plugins/blender/publish/validate_mesh_has_uv.py @@ -35,12 +35,15 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): invalid = [] # TODO (jasper): only check objects in the collection that will be published? for obj in [ - obj for obj in bpy.data.objects if obj.type == 'MESH' - ]: - # Make sure we are in object mode. - bpy.ops.object.mode_set(mode='OBJECT') - if not cls.has_uvs(obj): - invalid.append(obj) + obj for obj in instance]: + try: + if obj.type == 'MESH': + # Make sure we are in object mode. + bpy.ops.object.mode_set(mode='OBJECT') + if not cls.has_uvs(obj): + invalid.append(obj) + except: + continue return invalid def process(self, instance): From 28c626b69469c1b46e3939022ab7e997c2c7c09c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:59 +0100 Subject: [PATCH 148/327] attempt at alembic extractor --- pype/blender/plugin.py | 11 ++- pype/plugins/blender/publish/extract_abc.py | 91 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 pype/plugins/blender/publish/extract_abc.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index 8f72d04a1d..f27bf0daab 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -20,10 +20,15 @@ def asset_name( return name -def create_blender_context(obj: Optional[bpy.types.Object] = None): +def create_blender_context(active: Optional[bpy.types.Object] = None, + selected: Optional[bpy.types.Object] = None,): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ + + if not isinstance(selected, list): + selected = [selected] + for win in bpy.context.window_manager.windows: for area in win.screen.areas: if area.type == 'VIEW_3D': @@ -35,8 +40,8 @@ def create_blender_context(obj: Optional[bpy.types.Object] = None): 'area': area, 'region': region, 'scene': bpy.context.scene, - 'active_object': obj, - 'selected_objects': [obj] + 'active_object': selected[0], + 'selected_objects': selected } return override_context raise Exception("Could not create a custom Blender context.") diff --git a/pype/plugins/blender/publish/extract_abc.py b/pype/plugins/blender/publish/extract_abc.py new file mode 100644 index 0000000000..b953d41ba2 --- /dev/null +++ b/pype/plugins/blender/publish/extract_abc.py @@ -0,0 +1,91 @@ +import os + +import pype.api +import pype.blender.plugin + +import bpy + + +class ExtractABC(pype.api.Extractor): + """Extract as ABC.""" + + label = "Extract ABC" + hosts = ["blender"] + families = ["model"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + context = bpy.context + scene = context.scene + view_layer = context.view_layer + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" + + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + view_layer.active_layer_collection = layer_collections[0] + + old_scale = scene.unit_settings.scale_length + + selected = list() + + for obj in instance: + selected.append(obj) + + new_context = pype.blender.plugin.create_blender_context(active=None, selected=selected) + + # We set the scale of the scene for the export + scene.unit_settings.scale_length = 0.01 + + self.log.info(new_context) + + # We export the abc + bpy.ops.wm.alembic_export( + new_context, + filepath=filepath, + start=1, + end=1 + ) + + view_layer.active_layer_collection = old_active_layer_collection + + scene.unit_settings.scale_length = old_scale + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From 59dd912a7626e3e98238c11a4c38ebaf53162f23 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:26:40 +0100 Subject: [PATCH 149/327] tweak context override --- pype/blender/plugin.py | 2 +- pype/plugins/blender/publish/extract_abc.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index f27bf0daab..77fce90d65 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -40,7 +40,7 @@ def create_blender_context(active: Optional[bpy.types.Object] = None, 'area': area, 'region': region, 'scene': bpy.context.scene, - 'active_object': selected[0], + 'active_object': active, 'selected_objects': selected } return override_context diff --git a/pype/plugins/blender/publish/extract_abc.py b/pype/plugins/blender/publish/extract_abc.py index b953d41ba2..d2c0c769ae 100644 --- a/pype/plugins/blender/publish/extract_abc.py +++ b/pype/plugins/blender/publish/extract_abc.py @@ -55,9 +55,13 @@ class ExtractABC(pype.api.Extractor): selected = list() for obj in instance: - selected.append(obj) + try: + obj.select_set(True) + selected.append(obj) + except: + continue - new_context = pype.blender.plugin.create_blender_context(active=None, selected=selected) + new_context = pype.blender.plugin.create_blender_context(active=selected[0], selected=selected) # We set the scale of the scene for the export scene.unit_settings.scale_length = 0.01 From e5fd3d3b5df0f2717d5a566264940ddabcaf8ad9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 21 Mar 2020 19:13:53 +0100 Subject: [PATCH 150/327] frame range validator remake --- .../maya/publish/validate_frame_range.py | 158 +++++++++++++++--- 1 file changed, 135 insertions(+), 23 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index d4aad812d5..26235b37ae 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -1,18 +1,18 @@ import pyblish.api import pype.api +from maya import cmds + class ValidateFrameRange(pyblish.api.InstancePlugin): """Valides the frame ranges. - Checks the `startFrame`, `endFrame` and `handles` data. - This does NOT ensure there's actual data present. + This is optional validator checking if the frame range matches the one of + asset. - This validates: - - `startFrame` is lower than or equal to the `endFrame`. - - must have both the `startFrame` and `endFrame` data. - - The `handles` value is not lower than zero. + Repair action will change everything to match asset. + This can be turned off by artist to allow custom ranges. """ label = "Validate Frame Range" @@ -20,26 +20,138 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): families = ["animation", "pointcache", "camera", - "renderlayer", - "colorbleed.vrayproxy"] + "render", + "review", + "yeticache"] + optional = True + actions = [pype.api.RepairAction] def process(self, instance): + context = instance.context - start = instance.data.get("frameStart", None) - end = instance.data.get("frameEnd", None) - handles = instance.data.get("handles", None) + frame_start_handle = int(context.data.get("frameStartHandle")) + frame_end_handle = int(context.data.get("frameEndHandle")) + handles = int(context.data.get("handles")) + handle_start = int(context.data.get("handleStart")) + handle_end = int(context.data.get("handleEnd")) + frame_start = int(context.data.get("frameStart")) + frame_end = int(context.data.get("frameEnd")) - # Check if any of the values are present - if any(value is None for value in [start, end]): - raise ValueError("No time values for this instance. " - "(Missing `startFrame` or `endFrame`)") + inst_start = int(instance.data.get("frameStartHandle")) + inst_end = int(instance.data.get("frameEndHandle")) - self.log.info("Comparing start (%s) and end (%s)" % (start, end)) - if start > end: - raise RuntimeError("The start frame is a higher value " - "than the end frame: " - "{0}>{1}".format(start, end)) + # basic sanity checks + assert frame_start_handle <= frame_end_handle, ( + "start frame is lower then end frame") - if handles is not None: - if handles < 0.0: - raise RuntimeError("Handles are set to a negative value") + assert handles >= 0, ("handles cannot have negative values") + + # compare with data on instance + errors = [] + + if(inst_start != frame_start_handle): + errors.append("Instance start frame [ {} ] doesn't " + "match the one set on instance [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + inst_start, + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if(inst_end != frame_end_handle): + errors.append("Instance end frame [ {} ] doesn't " + "match the one set on instance [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + inst_end, + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + minTime = int(cmds.playbackOptions(minTime=True, query=True)) + maxTime = int(cmds.playbackOptions(maxTime=True, query=True)) + animStartTime = int(cmds.playbackOptions(animationStartTime=True, + query=True)) + animEndTime = int(cmds.playbackOptions(animationEndTime=True, + query=True)) + + if int(minTime) != inst_start: + errors.append("Start of Maya timeline is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + minTime, + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(maxTime) != inst_end: + errors.append("End of Maya timeline is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + maxTime, + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(animStartTime) != inst_start: + errors.append("Animation start in Maya is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + animStartTime, + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(animEndTime) != inst_end: + errors.append("Animation start in Maya is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + animEndTime, + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) + render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) + + if int(render_start) != inst_start: + errors.append("Render settings start frame is set to [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_start), + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(render_end) != inst_end: + errors.append("Render settings end frame is set to [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_end), + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + for e in errors: + self.log.error(e) + + assert len(errors) == 0, ("Frame range settings are incorrect") + + @classmethod + def repair(cls, instance): + """ + Repair by calling avalon reset frame range function. This will set + timeline frame range, render settings range and frame information + on instance container to match asset data. + """ + import avalon.maya.interactive + avalon.maya.interactive.reset_frame_range() + cls.log.debug("-" * 80) + cls.log.debug("{}.frameStart".format(instance.data["name"])) + cmds.setAttr( + "{}.frameStart".format(instance.data["name"]), + instance.context.data.get("frameStartHandle")) + + cmds.setAttr( + "{}.frameEnd".format(instance.data["name"]), + instance.context.data.get("frameEndHandle")) + cls.log.debug("-" * 80) From 8d32d77668ebb7b535f5b660d72bfec367788d30 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 21 Mar 2020 21:00:39 +0100 Subject: [PATCH 151/327] remove debug prints --- pype/plugins/maya/publish/validate_frame_range.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index 26235b37ae..0be77644a0 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -145,8 +145,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): """ import avalon.maya.interactive avalon.maya.interactive.reset_frame_range() - cls.log.debug("-" * 80) - cls.log.debug("{}.frameStart".format(instance.data["name"])) cmds.setAttr( "{}.frameStart".format(instance.data["name"]), instance.context.data.get("frameStartHandle")) @@ -154,4 +152,3 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): cmds.setAttr( "{}.frameEnd".format(instance.data["name"]), instance.context.data.get("frameEndHandle")) - cls.log.debug("-" * 80) From 0e991c4fb572ebfb3a54c8d9e8b882f8377fdba1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 21 Mar 2020 23:21:41 +0100 Subject: [PATCH 152/327] shutting up hound --- .../plugins/maya/publish/collect_instances.py | 9 ++++---- pype/plugins/maya/publish/collect_render.py | 21 ++++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/pype/plugins/maya/publish/collect_instances.py b/pype/plugins/maya/publish/collect_instances.py index 9ea3ebe7fa..1d59a68bf6 100644 --- a/pype/plugins/maya/publish/collect_instances.py +++ b/pype/plugins/maya/publish/collect_instances.py @@ -122,7 +122,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # if frame range on maya set is the same as full shot range # adjust the values to match the asset data if (ctx_frame_start_handle == data["frameStart"] - and ctx_frame_end_handle == data["frameEnd"]): + and ctx_frame_end_handle == data["frameEnd"]): # noqa: W503, E501 data["frameStartHandle"] = ctx_frame_start_handle data["frameEndHandle"] = ctx_frame_end_handle data["frameStart"] = ctx_frame_start @@ -141,8 +141,8 @@ class CollectInstances(pyblish.api.ContextPlugin): data["handleStart"] = 0 data["handleEnd"] = 0 - data["frameStartHandle"] = data["frameStart"] - data["handleStart"] - data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + data["frameStartHandle"] = data["frameStart"] - data["handleStart"] # noqa: E501 + data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] # noqa: E501 if "handles" in data: data.pop('handles') @@ -157,7 +157,8 @@ class CollectInstances(pyblish.api.ContextPlugin): # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: {} ".format(json.dumps(instance.data, indent=4))) + self.log.debug( + "DATA: {} ".format(json.dumps(instance.data, indent=4))) def sort_by_family(instance): """Sort by family""" diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 88c1be477d..365b0b5a13 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -203,13 +203,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths.append(full_path) aov_dict["beauty"] = full_paths - frame_start_render = int(self.get_render_attribute("startFrame", - layer=layer_name)) - frame_end_render = int(self.get_render_attribute("endFrame", - layer=layer_name)) + frame_start_render = int(self.get_render_attribute( + "startFrame", layer=layer_name)) + frame_end_render = int(self.get_render_attribute( + "endFrame", layer=layer_name)) - if (int(context.data['frameStartHandle']) == frame_start_render and - int(context.data['frameEndHandle']) == frame_end_render): + if (int(context.data['frameStartHandle']) == frame_start_render + and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 handle_start = context.data['handleStart'] handle_end = context.data['handleEnd'] @@ -506,7 +506,7 @@ class AExpectedFiles: expected_files.append( '{}.{}.{}'.format(file_prefix, str(frame).rjust( - layer_data["padding"], "0"), + layer_data["padding"], "0"), layer_data["defaultExt"])) return expected_files @@ -642,7 +642,7 @@ class ExpectedFilesArnold(AExpectedFiles): enabled_aovs = [] try: if not (cmds.getAttr('defaultArnoldRenderOptions.aovMode') - and not cmds.getAttr('defaultArnoldDriver.mergeAOVs')): + and not cmds.getAttr('defaultArnoldDriver.mergeAOVs')): # noqa: W503, E501 # AOVs are merged in mutli-channel file return enabled_aovs except ValueError: @@ -763,10 +763,7 @@ class ExpectedFilesVray(AExpectedFiles): if enabled: # todo: find how vray set format for AOVs enabled_aovs.append( - ( - self._get_vray_aov_name(aov), - default_ext) - ) + (self._get_vray_aov_name(aov), default_ext)) return enabled_aovs def _get_vray_aov_name(self, node): From 80aa8a52b755b36a11769d2a4e81ee7fb3b189e8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 23 Mar 2020 11:58:11 +0100 Subject: [PATCH 153/327] validate only important things --- .../maya/publish/validate_frame_range.py | 90 ++++++------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index 0be77644a0..c0a43fe4c7 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -7,8 +7,9 @@ from maya import cmds class ValidateFrameRange(pyblish.api.InstancePlugin): """Valides the frame ranges. - This is optional validator checking if the frame range matches the one of - asset. + This is optional validator checking if the frame range on instance + matches the one of asset. It also validate render frame range of render + layers Repair action will change everything to match asset. @@ -20,7 +21,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): families = ["animation", "pointcache", "camera", - "render", + "renderlayer", "review", "yeticache"] optional = True @@ -67,69 +68,32 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): handle_start, frame_start, frame_end, handle_end )) - minTime = int(cmds.playbackOptions(minTime=True, query=True)) - maxTime = int(cmds.playbackOptions(maxTime=True, query=True)) - animStartTime = int(cmds.playbackOptions(animationStartTime=True, - query=True)) - animEndTime = int(cmds.playbackOptions(animationEndTime=True, - query=True)) + if "renderlayer" in self.families: - if int(minTime) != inst_start: - errors.append("Start of Maya timeline is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - minTime, - frame_start_handle, - handle_start, frame_start, frame_end, handle_end - )) + render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) + render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - if int(maxTime) != inst_end: - errors.append("End of Maya timeline is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - maxTime, - frame_end_handle, - handle_start, frame_start, frame_end, handle_end - )) + if int(render_start) != inst_start: + errors.append("Render settings start frame is set to [ {} ] " + "and doesn't match the one set on " + "asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_start), + frame_start_handle, + handle_start, frame_start, frame_end, + handle_end + )) - if int(animStartTime) != inst_start: - errors.append("Animation start in Maya is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - animStartTime, - frame_start_handle, - handle_start, frame_start, frame_end, handle_end - )) - - if int(animEndTime) != inst_end: - errors.append("Animation start in Maya is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - animEndTime, - frame_end_handle, - handle_start, frame_start, frame_end, handle_end - )) - - render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) - render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - - if int(render_start) != inst_start: - errors.append("Render settings start frame is set to [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_start), - frame_start_handle, - handle_start, frame_start, frame_end, handle_end - )) - - if int(render_end) != inst_end: - errors.append("Render settings end frame is set to [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_end), - frame_end_handle, - handle_start, frame_start, frame_end, handle_end - )) + if int(render_end) != inst_end: + errors.append("Render settings end frame is set to [ {} ] " + "and doesn't match the one set on " + "asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_end), + frame_end_handle, + handle_start, frame_start, frame_end, + handle_end + )) for e in errors: self.log.error(e) From 05213b591f53bb3809e3b262ad1fcbbfe4ca187c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 23 Mar 2020 13:19:53 +0100 Subject: [PATCH 154/327] fix(nuke): better exception logic --- pype/nuke/lib.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 8e241dad16..6cd66407d6 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -329,8 +329,17 @@ def create_write_node(name, data, input=None, prenodes=None): # add data to knob for k, v in properties: + try: + now_node[k].value() + except NameError: + log.warning( + "knob `{}` does not exist on node `{}`".format( + k, now_node["name"].value() + )) + continue + if k and v: - now_node[k].serValue(str(v)) + now_node[k].setValue(str(v)) # connect to previous node if set_output_to: @@ -339,14 +348,14 @@ def create_write_node(name, data, input=None, prenodes=None): input_node = nuke.createNode( "Input", "name {}".format(node_name)) connections.append({ - "node": nuke.toNode(node_name), + "node": nuke.toNode(node_name), "inputName": node_name}) now_node.setInput(1, input_node) elif isinstance(set_output_to, str): input_node = nuke.createNode( "Input", "name {}".format(node_name)) connections.append({ - "node": nuke.toNode(set_output_to), + "node": nuke.toNode(set_output_to), "inputName": set_output_to}) now_node.setInput(0, input_node) else: From f2eb13e3ca595525cad4b7a9ec7a2b77cb566a9a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 23 Mar 2020 14:57:25 +0100 Subject: [PATCH 155/327] hound cleanups --- .../publish/integrate_master_version.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 16aa0dd23d..3c7838b708 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -17,12 +17,14 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): optional = True - families = ["model", - "rig", - "setdress", - "look", - "pointcache", - "animation"] + families = [ + "model", + "rig", + "setdress", + "look", + "pointcache", + "animation" + ] # Can specify representation names that will be ignored (lower case) ignored_representation_names = [] @@ -109,13 +111,13 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): all_copied_files = [] transfers = instance.data.get("transfers", list()) - for src, dst in transfers: + for dst in transfers.values(): dst = os.path.normpath(dst) if dst not in all_copied_files: all_copied_files.append(dst) hardlinks = instance.data.get("hardlinks", list()) - for src, dst in hardlinks: + for dst in hardlinks.values(): dst = os.path.normpath(dst) if dst not in all_copied_files: all_copied_files.append(dst) @@ -190,7 +192,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): # Separate old representations into `to replace` and `to delete` old_repres_to_replace = {} old_repres_to_delete = {} - for repre_id, repre_info in published_repres.items(): + for repre_info in published_repres.values(): repre = repre_info["representation"] repre_name_low = repre["name"].lower() if repre_name_low in old_repres_by_name: @@ -260,7 +262,7 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): )) try: src_to_dst_file_paths = [] - for repre_id, repre_info in published_repres.items(): + for repre_info in published_repres.values(): # Skip if new repre does not have published repre files published_files = repre_info["published_files"] From 566d4952486a24cd8e3e867490a9647fac57a582 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 23 Mar 2020 18:06:26 +0100 Subject: [PATCH 156/327] simplified frame range validator --- .../maya/publish/validate_frame_range.py | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index c0a43fe4c7..0d51a83cf5 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -68,33 +68,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): handle_start, frame_start, frame_end, handle_end )) - if "renderlayer" in self.families: - - render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) - render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - - if int(render_start) != inst_start: - errors.append("Render settings start frame is set to [ {} ] " - "and doesn't match the one set on " - "asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_start), - frame_start_handle, - handle_start, frame_start, frame_end, - handle_end - )) - - if int(render_end) != inst_end: - errors.append("Render settings end frame is set to [ {} ] " - "and doesn't match the one set on " - "asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_end), - frame_end_handle, - handle_start, frame_start, frame_end, - handle_end - )) - for e in errors: self.log.error(e) @@ -103,12 +76,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): """ - Repair by calling avalon reset frame range function. This will set - timeline frame range, render settings range and frame information - on instance container to match asset data. + Repair instance container to match asset data. """ - import avalon.maya.interactive - avalon.maya.interactive.reset_frame_range() cmds.setAttr( "{}.frameStart".format(instance.data["name"]), instance.context.data.get("frameStartHandle")) From 4511b915035cdb509562b84d088f7f8834c04c8a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 23 Mar 2020 18:32:20 +0100 Subject: [PATCH 157/327] set avalon project on publish job by environment variable --- .../global/publish/submit_publish_job.py | 3 ++- pype/scripts/publish_filesequence.py | 18 ------------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 556132cd77..9cfeb0762e 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -222,9 +222,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment - environment = job["Props"].get("Env", {}) environment["PYPE_METADATA_FILE"] = metadata_path + environment["AVALON_PROJECT"] = api.Session.get("AVALON_PROJECT") + i = 0 for index, key in enumerate(environment): if key.upper() in self.enviro_filter: diff --git a/pype/scripts/publish_filesequence.py b/pype/scripts/publish_filesequence.py index fe795564a5..a41d97668e 100644 --- a/pype/scripts/publish_filesequence.py +++ b/pype/scripts/publish_filesequence.py @@ -25,18 +25,6 @@ log.setLevel(logging.DEBUG) error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" -def _load_json(path): - assert os.path.isfile(path), ("path to json file doesn't exist") - data = None - with open(path, "r") as json_file: - try: - data = json.load(json_file) - except Exception as exc: - log.error( - "Error loading json: " - "{} - Exception: {}".format(path, exc) - ) - return data def __main__(): parser = argparse.ArgumentParser() @@ -90,12 +78,6 @@ def __main__(): paths = kwargs.paths or [os.environ.get("PYPE_METADATA_FILE")] or [os.getcwd()] # noqa - for path in paths: - data = _load_json(path) - log.info("Setting session using data from file") - os.environ["AVALON_PROJECT"] = data["session"]["AVALON_PROJECT"] - break - args = [ os.path.join(pype_root, pype_command), "publish", From f1241415f991f41f06e440b7ce9a677bfa079530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 23 Mar 2020 18:38:03 +0100 Subject: [PATCH 158/327] fixing flake8 configuration there was duplicated ignore option --- .flake8 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index f28d8cbfc3..b04062ceab 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,6 @@ [flake8] # ignore = D203 -ignore = BLK100 -ignore = W504 +ignore = BLK100, W504 max-line-length = 79 exclude = .git, From d1dd5023571b10934c0b81dc4d8cd31e6983998f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 16:35:37 +0100 Subject: [PATCH 159/327] fix(nuke): validator clam fps float number --- pype/plugins/nuke/publish/validate_script.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py index f7dd84d714..36df228ed5 100644 --- a/pype/plugins/nuke/publish/validate_script.py +++ b/pype/plugins/nuke/publish/validate_script.py @@ -74,17 +74,14 @@ class ValidateScript(pyblish.api.InstancePlugin): if "handleEnd" in asset_attributes: handle_end = asset_attributes["handleEnd"] - # Set frame range with handles - # asset_attributes["frameStart"] -= handle_start - # asset_attributes["frameEnd"] += handle_end - if len(str(asset_attributes["fps"])) > 4: - asset_attributes["fps"] = float("{0:.8f}".format(asset_attributes["fps"])) + asset_attributes["fps"] = float("{0:.4f}".format( + asset_attributes["fps"])) # Get values from nukescript script_attributes = { "handleStart": ctx_data["handleStart"], "handleEnd": ctx_data["handleEnd"], - "fps": ctx_data["fps"], + "fps": float("{0:.4f}".format(ctx_data["fps"])), "frameStart": ctx_data["frameStart"], "frameEnd": ctx_data["frameEnd"], "resolutionWidth": ctx_data["resolutionWidth"], From 139add3a45d353c211ef39264d03b589227d9d62 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Mar 2020 17:23:36 +0100 Subject: [PATCH 160/327] it is possible to use `source_timecode` in burnins --- pype/scripts/otio_burnin.py | 48 ++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 8b52216968..7c94006466 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -36,7 +36,8 @@ TIMECODE = ( MISSING_KEY_VALUE = "N/A" CURRENT_FRAME_KEY = "{current_frame}" CURRENT_FRAME_SPLITTER = "_-_CURRENT_FRAME_-_" -TIME_CODE_KEY = "{timecode}" +TIMECODE_KEY = "{timecode}" +SOURCE_TIMECODE_KEY = "{source_timecode}" def _streams(source): @@ -188,10 +189,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if not options.get("fps"): options["fps"] = self.frame_rate - options["timecode"] = ffmpeg_burnins._frames_to_timecode( - frame_start_tc, - self.frame_rate - ) + if isinstance(frame_start_tc, str): + options["timecode"] = frame_start_tc + else: + options["timecode"] = ffmpeg_burnins._frames_to_timecode( + frame_start_tc, + self.frame_rate + ) self._add_burnin(text, align, options, TIMECODE) @@ -412,7 +416,14 @@ def burnins_from_data( data[CURRENT_FRAME_KEY[1:-1]] = CURRENT_FRAME_SPLITTER if frame_start_tc is not None: - data[TIME_CODE_KEY[1:-1]] = TIME_CODE_KEY + data[TIMECODE_KEY[1:-1]] = TIMECODE_KEY + + source_timecode = stream.get("timecode") + if source_timecode is None: + source_timecode = stream.get("tags", {}).get("timecode") + + if source_timecode is not None: + data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY for align_text, value in presets.get('burnins', {}).items(): if not value: @@ -425,8 +436,6 @@ def burnins_from_data( " (Make sure you have new burnin presets)." ).format(str(type(value)), str(value))) - has_timecode = TIME_CODE_KEY in value - align = None align_text = align_text.strip().lower() if align_text == "top_left": @@ -442,6 +451,7 @@ def burnins_from_data( elif align_text == "bottom_right": align = ModifiedBurnins.BOTTOM_RIGHT + has_timecode = TIMECODE_KEY in value # Replace with missing key value if frame_start_tc is not set if frame_start_tc is None and has_timecode: has_timecode = False @@ -449,7 +459,13 @@ def burnins_from_data( "`frame_start` and `frame_start_tc`" " are not set in entered data." ) - value = value.replace(TIME_CODE_KEY, MISSING_KEY_VALUE) + value = value.replace(TIMECODE_KEY, MISSING_KEY_VALUE) + + has_source_timecode = SOURCE_TIMECODE_KEY in value + if source_timecode is None and has_source_timecode: + has_source_timecode = False + log.warning("Source does not have set timecode value.") + value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE) key_pattern = re.compile(r"(\{.*?[^{0]*\})") @@ -465,10 +481,20 @@ def burnins_from_data( value = value.replace(key, MISSING_KEY_VALUE) # Handle timecode differently + if has_source_timecode: + args = [align, frame_start, frame_end, source_timecode] + if not value.startswith(SOURCE_TIMECODE_KEY): + value_items = value.split(SOURCE_TIMECODE_KEY) + text = value_items[0].format(**data) + args.append(text) + + burnin.add_timecode(*args) + continue + if has_timecode: args = [align, frame_start, frame_end, frame_start_tc] - if not value.startswith(TIME_CODE_KEY): - value_items = value.split(TIME_CODE_KEY) + if not value.startswith(TIMECODE_KEY): + value_items = value.split(TIMECODE_KEY) text = value_items[0].format(**data) args.append(text) From 695351cefcf25e3bc564f4f35fb87c6efeebfcb9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 18:27:11 +0100 Subject: [PATCH 161/327] feat(nks): improving way file's metadata are collected --- .../nukestudio/publish/collect_clips.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pype/plugins/nukestudio/publish/collect_clips.py b/pype/plugins/nukestudio/publish/collect_clips.py index 6a1dad9a6d..746df67485 100644 --- a/pype/plugins/nukestudio/publish/collect_clips.py +++ b/pype/plugins/nukestudio/publish/collect_clips.py @@ -47,6 +47,16 @@ class CollectClips(api.ContextPlugin): track = item.parent() source = item.source().mediaSource() source_path = source.firstpath() + file_head = source.filenameHead() + file_info = next((f for f in source.fileinfos()), None) + source_first_frame = file_info.startFrame() + is_sequence = False + + if not source.singleFile(): + self.log.info("Single file") + is_sequence = True + source_path = file_info.filename() + effects = [f for f in item.linkedItems() if f.isEnabled() if isinstance(f, hiero.core.EffectTrackItem)] @@ -78,12 +88,6 @@ class CollectClips(api.ContextPlugin): ) ) - try: - head, padding, ext = os.path.basename(source_path).split(".") - source_first_frame = int(padding) - except Exception: - source_first_frame = 0 - data.update({ "name": "{0}_{1}".format(track.name(), item.name()), "item": item, @@ -91,6 +95,8 @@ class CollectClips(api.ContextPlugin): "timecodeStart": str(source.timecodeStart()), "timelineTimecodeStart": str(sequence.timecodeStart()), "sourcePath": source_path, + "sourceFileHead": file_head, + "isSequence": is_sequence, "track": track.name(), "trackIndex": track_index, "sourceFirst": source_first_frame, @@ -101,8 +107,9 @@ class CollectClips(api.ContextPlugin): int(item.sourceIn())) + 1, "clipIn": int(item.timelineIn()), "clipOut": int(item.timelineOut()), - "clipDuration": (int(item.timelineOut()) - - int(item.timelineIn())) + 1, + "clipDuration": ( + int(item.timelineOut()) - int( + item.timelineIn())) + 1, "asset": asset, "family": "clip", "families": [], From c6c4c26a54386b0b05ec147942ecd5ced97fc4f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 18:27:45 +0100 Subject: [PATCH 162/327] fix(nks): plate single file vs sequence treatment --- .../nukestudio/publish/collect_plates.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py index 4ed281f0ee..8a79354bbf 100644 --- a/pype/plugins/nukestudio/publish/collect_plates.py +++ b/pype/plugins/nukestudio/publish/collect_plates.py @@ -147,22 +147,15 @@ class CollectPlatesData(api.InstancePlugin): "version": version }) + source_first_frame = instance.data.get("sourceFirst") + source_file_head = instance.data.get("sourceFileHead") - try: - basename, ext = os.path.splitext(source_file) - head, padding = os.path.splitext(basename) - ext = ext[1:] - padding = padding[1:] - self.log.debug("_ padding: `{}`".format(padding)) - # head, padding, ext = source_file.split('.') - source_first_frame = int(padding) - padding = len(padding) - file = "{head}.%0{padding}d.{ext}".format( - head=head, - padding=padding, - ext=ext - ) - + if instance.data.get("isSequence", False): + self.log.info("Is sequence of files") + file = os.path.basename(source_file) + ext = os.path.splitext(file)[-1][1:] + self.log.debug("source_file_head: `{}`".format(source_file_head)) + head = source_file_head[:-1] start_frame = int(source_first_frame + instance.data["sourceInH"]) duration = int( instance.data["sourceOutH"] - instance.data["sourceInH"]) @@ -170,10 +163,10 @@ class CollectPlatesData(api.InstancePlugin): self.log.debug("start_frame: `{}`".format(start_frame)) self.log.debug("end_frame: `{}`".format(end_frame)) files = [file % i for i in range(start_frame, (end_frame + 1), 1)] - except Exception as e: - self.log.warning("Exception in file: {}".format(e)) - head, ext = os.path.splitext(source_file) - ext = ext[1:] + else: + self.log.info("Is single file") + ext = os.path.splitext(source_file)[-1][1:] + head = source_file_head files = source_file start_frame = instance.data["sourceInH"] end_frame = instance.data["sourceOutH"] From 461441ebf42ab734f741acd764739e51f2d805c8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 19:47:36 +0100 Subject: [PATCH 163/327] fix(nk): build first workfile get jpeg sequences --- pype/nuke/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 6cd66407d6..cbec8b4300 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1135,7 +1135,7 @@ class BuildWorkfile(WorkfileSettings): regex_filter=None, version=None, representations=["exr", "dpx", "lutJson", "mov", - "preview", "png"]): + "preview", "png", "jpeg", "jpg"]): """ A short description. From f80e4e123546de2cf40d2c1f0d5c488cc9055f25 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 11:13:54 +0100 Subject: [PATCH 164/327] clean(nuke): pep8 improvments --- pype/nuke/lib.py | 29 +++++++++++++++-------------- setup/nuke/nuke_path/menu.py | 10 +++++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index ad2d576da3..ccb4f343db 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -28,7 +28,7 @@ self = sys.modules[__name__] self._project = None -def onScriptLoad(): +def on_script_load(): ''' Callback for ffmpeg support ''' if nuke.env['LINUX']: @@ -39,7 +39,7 @@ def onScriptLoad(): nuke.tcl('load movWriter') -def checkInventoryVersions(): +def check_inventory_versions(): """ Actiual version idetifier of Loaded containers @@ -180,8 +180,8 @@ def format_anatomy(data): padding = int(anatomy.templates['render']['padding']) except KeyError as e: msg = ("`padding` key is not in `render` " - "Anatomy template. Please, add it there and restart " - "the pipeline (padding: \"4\"): `{}`").format(e) + "Anatomy template. Please, add it there and restart " + "the pipeline (padding: \"4\"): `{}`").format(e) log.error(msg) nuke.message(msg) @@ -701,7 +701,8 @@ class WorkfileSettings(object): def set_reads_colorspace(self, reads): """ Setting colorspace to Read nodes - Looping trought all read nodes and tries to set colorspace based on regex rules in presets + Looping trought all read nodes and tries to set colorspace based + on regex rules in presets """ changes = dict() for n in nuke.allNodes(): @@ -873,10 +874,10 @@ class WorkfileSettings(object): if any(x for x in data.values() if x is None): msg = ("Missing set shot attributes in DB." - "\nContact your supervisor!." - "\n\nWidth: `{width}`" - "\nHeight: `{height}`" - "\nPixel Asspect: `{pixel_aspect}`").format(**data) + "\nContact your supervisor!." + "\n\nWidth: `{width}`" + "\nHeight: `{height}`" + "\nPixel Asspect: `{pixel_aspect}`").format(**data) log.error(msg) nuke.message(msg) @@ -895,8 +896,9 @@ class WorkfileSettings(object): ) except Exception as e: bbox = None - msg = ("{}:{} \nFormat:Crop need to be set with dots, example: " - "0.0.1920.1080, /nSetting to default").format(__name__, e) + msg = ("{}:{} \nFormat:Crop need to be set with dots, " + "example: 0.0.1920.1080, " + "/nSetting to default").format(__name__, e) log.error(msg) nuke.message(msg) @@ -1037,7 +1039,8 @@ class BuildWorkfile(WorkfileSettings): """ Building first version of workfile. - Settings are taken from presets and db. It will add all subsets in last version for defined representaions + Settings are taken from presets and db. It will add all subsets + in last version for defined representaions Arguments: variable (type): description @@ -1265,8 +1268,6 @@ class BuildWorkfile(WorkfileSettings): representation (dict): avalon db entity """ - context = representation["context"] - loader_name = "LoadLuts" loader_plugin = None diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 15702fa364..be4f39b542 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -4,8 +4,8 @@ import KnobScripter from pype.nuke.lib import ( writes_version_sync, - onScriptLoad, - checkInventoryVersions + on_script_load, + check_inventory_versions ) import nuke @@ -15,9 +15,9 @@ log = Logger().get_logger(__name__, "nuke") # nuke.addOnScriptSave(writes_version_sync) -nuke.addOnScriptSave(onScriptLoad) -nuke.addOnScriptLoad(checkInventoryVersions) -nuke.addOnScriptSave(checkInventoryVersions) +nuke.addOnScriptSave(on_script_load) +nuke.addOnScriptLoad(check_inventory_versions) +nuke.addOnScriptSave(check_inventory_versions) # nuke.addOnScriptSave(writes_version_sync) log.info('Automatic syncing of write file knob to script version') From 3d0b7e49c7cadc849b3d165443e16923ae33309f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 25 Mar 2020 13:52:40 +0100 Subject: [PATCH 165/327] PEP fixes --- .flake8 | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 7 +++---- pype/plugins/unreal/load/load_staticmeshfbx.py | 4 ++-- pype/unreal/lib.py | 10 +++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.flake8 b/.flake8 index b04062ceab..f9c81de232 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] # ignore = D203 -ignore = BLK100, W504 +ignore = BLK100, W504, W503 max-line-length = 79 exclude = .git, diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 58c550b3dd..b5576ae046 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -277,7 +277,7 @@ class AppAction(BaseHandler): 'success': False, 'message': "Hook didn't finish successfully {0}" .format(self.label) - } + } if sys.platform == "win32": @@ -290,7 +290,7 @@ class AppAction(BaseHandler): # Run SW if was found executable if execfile is not None: - popen = avalonlib.launch( + avalonlib.launch( executable=execfile, args=[], environment=env ) else: @@ -298,8 +298,7 @@ class AppAction(BaseHandler): 'success': False, 'message': "We didn't found launcher for {0}" .format(self.label) - } - pass + } if sys.platform.startswith('linux'): execfile = os.path.join(path.strip('"'), self.executable) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py index 61e765f7c2..4c27f9aa92 100644 --- a/pype/plugins/unreal/load/load_staticmeshfbx.py +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -37,7 +37,7 @@ class StaticMeshFBXLoader(api.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() temp_dir, temp_name = tools.create_unique_asset_name( - "/Game/{}".format(name), "_TMP" + "/Game/{}".format(name), "_TMP" ) unreal.EditorAssetLibrary.make_directory(temp_dir) @@ -95,7 +95,7 @@ class StaticMeshFBXLoader(api.Loader): container["objectName"]) # update metadata avalon_unreal.imprint( - container_path, {"_id": str(representation["_id"])}) + container_path, {"_id": str(representation["_id"])}) def remove(self, container): unreal.EditorAssetLibrary.delete_directory(container["namespace"]) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 8f87fdbf4e..0b049c8b1d 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -214,11 +214,11 @@ def create_unreal_project(project_name: str, # sources at start data["Modules"] = [{ - "Name": project_name, - "Type": "Runtime", - "LoadingPhase": "Default", - "AdditionalDependencies": ["Engine"], - }] + "Name": project_name, + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": ["Engine"], + }] if preset["install_unreal_python_engine"]: # now we need to fix python path in: From fc86c46af00598b67ea633a37e0ac12c6fa59882 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 25 Mar 2020 14:03:58 +0100 Subject: [PATCH 166/327] missing_options_from_look_loader --- pype/plugins/maya/load/load_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index 04ac9b23e4..c31b7c5fe0 100644 --- a/pype/plugins/maya/load/load_look.py +++ b/pype/plugins/maya/load/load_look.py @@ -16,7 +16,7 @@ class LookLoader(pype.maya.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def process_reference(self, context, name, namespace, data): + def process_reference(self, context, name, namespace, options): """ Load and try to assign Lookdev to nodes based on relationship data Args: From 570d336f05ad5fb3b2fb8249e06bf1e3cee6ec1b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Mar 2020 15:29:50 +0100 Subject: [PATCH 167/327] fix invalid iterations in integrate master version --- pype/plugins/global/publish/integrate_master_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_master_version.py b/pype/plugins/global/publish/integrate_master_version.py index 3c7838b708..af6e7707e4 100644 --- a/pype/plugins/global/publish/integrate_master_version.py +++ b/pype/plugins/global/publish/integrate_master_version.py @@ -111,13 +111,13 @@ class IntegrateMasterVersion(pyblish.api.InstancePlugin): all_copied_files = [] transfers = instance.data.get("transfers", list()) - for dst in transfers.values(): + for _src, dst in transfers: dst = os.path.normpath(dst) if dst not in all_copied_files: all_copied_files.append(dst) hardlinks = instance.data.get("hardlinks", list()) - for dst in hardlinks.values(): + for _src, dst in hardlinks: dst = os.path.normpath(dst) if dst not in all_copied_files: all_copied_files.append(dst) From c15437f04a0bf215871a2df205ba61e52227521e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Mar 2020 16:32:06 +0100 Subject: [PATCH 168/327] template_data stores frame used for anatomy filling --- pype/plugins/global/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index ae946f0696..32504a64b3 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -326,6 +326,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): test_dest_files.append( os.path.normpath(template_filled) ) + # Store used frame value to template data + template_data["frame"] = repre_context["frame"] self.log.debug( "test_dest_files: {}".format(str(test_dest_files))) From ac47874d3c7a1f9c6a527236fd29a6344c48e3de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Mar 2020 16:32:34 +0100 Subject: [PATCH 169/327] template_name could not be overriden during representation loop --- pype/plugins/global/publish/integrate_new.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 32504a64b3..d4e4ae0b87 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -89,6 +89,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" ] + default_template_name = "publish" def process(self, instance): @@ -261,7 +262,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Each should be a single representation (as such, a single extension) representations = [] destination_list = [] - template_name = 'publish' + if 'transfers' not in instance.data: instance.data['transfers'] = [] @@ -288,8 +289,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): files = repre['files'] if repre.get('stagingDir'): stagingdir = repre['stagingDir'] - if repre.get('anatomy_template'): - template_name = repre['anatomy_template'] + + template_name = ( + repre.get('anatomy_template') or self.default_template_name + ) if repre.get("outputName"): template_data["output"] = repre['outputName'] From 0cb481706ca0d88e0e5fbb2aa7938b904440707d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Mar 2020 16:37:55 +0100 Subject: [PATCH 170/327] Now is used corrent frame --- pype/plugins/global/publish/integrate_new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index d4e4ae0b87..768970ccdc 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -329,8 +329,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): test_dest_files.append( os.path.normpath(template_filled) ) - # Store used frame value to template data - template_data["frame"] = repre_context["frame"] self.log.debug( "test_dest_files: {}".format(str(test_dest_files))) @@ -387,6 +385,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not dst_start_frame: dst_start_frame = dst_padding + # Store used frame value to template data + template_data["frame"] = dst_start_frame dst = "{0}{1}{2}".format( dst_head, dst_start_frame, From 9577c236243d7caa42a4bdd1409e469d2f1cd0d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 18:07:12 +0100 Subject: [PATCH 171/327] fix(global): slate was on even if render on farm --- pype/plugins/global/publish/extract_review_slate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_review_slate.py b/pype/plugins/global/publish/extract_review_slate.py index 8c33a0d853..da94c7714a 100644 --- a/pype/plugins/global/publish/extract_review_slate.py +++ b/pype/plugins/global/publish/extract_review_slate.py @@ -11,7 +11,9 @@ class ExtractReviewSlate(pype.api.Extractor): label = "Review with Slate frame" order = pyblish.api.ExtractorOrder + 0.031 - families = ["slate"] + families = ["slate", "review"] + match = pyblish.api.Subset + hosts = ["nuke", "maya", "shell"] optional = True From 570269f0247331460ad66baee0a76728bba0242b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 18:56:17 +0100 Subject: [PATCH 172/327] fix(global, nuke): setting families filter --- .../publish/integrate_ftrack_instances.py | 1 + pype/plugins/global/publish/integrate_new.py | 2 -- pype/plugins/nuke/load/load_mov.py | 1 + pype/plugins/nuke/load/load_sequence.py | 4 ++-- .../nuke/publish/increment_script_version.py | 21 +++++-------------- .../nuke/publish/validate_rendered_frames.py | 2 +- 6 files changed, 10 insertions(+), 21 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index db257e901a..59fb507788 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -22,6 +22,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): 'setdress': 'setdress', 'pointcache': 'cache', 'render': 'render', + 'render2d': 'render', 'nukescript': 'comp', 'write': 'render', 'review': 'mov', diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 127bff90f0..0ceac1f4a7 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -64,9 +64,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "scene", "vrayproxy", "render", - "render.local", "prerender", - "prerender.local", "imagesequence", "review", "rendersetup", diff --git a/pype/plugins/nuke/load/load_mov.py b/pype/plugins/nuke/load/load_mov.py index 88e65156cb..5d15efcd3a 100644 --- a/pype/plugins/nuke/load/load_mov.py +++ b/pype/plugins/nuke/load/load_mov.py @@ -92,6 +92,7 @@ class LoadMov(api.Loader): "source", "plate", "render", + "prerender", "review"] + presets["families"] representations = [ diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 690f074c3f..083cc86474 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -70,7 +70,7 @@ def loader_shift(node, frame, relative=True): class LoadSequence(api.Loader): """Load image sequence into Nuke""" - families = ["render2d", "source", "plate", "render"] + families = ["render2d", "source", "plate", "render", "prerender"] representations = ["exr", "dpx", "jpg", "jpeg", "png"] label = "Load sequence" @@ -87,7 +87,7 @@ class LoadSequence(api.Loader): version = context['version'] version_data = version.get("data", {}) repr_id = context["representation"]["_id"] - + self.log.info("version_data: {}\n".format(version_data)) self.log.debug( "Representation id `{}` ".format(repr_id)) diff --git a/pype/plugins/nuke/publish/increment_script_version.py b/pype/plugins/nuke/publish/increment_script_version.py index 6e3ce08276..c76083eb1e 100644 --- a/pype/plugins/nuke/publish/increment_script_version.py +++ b/pype/plugins/nuke/publish/increment_script_version.py @@ -9,6 +9,7 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.9 label = "Increment Script Version" optional = True + families = ["workfile", "render", "render.local", "render.farm"] hosts = ['nuke'] def process(self, context): @@ -16,19 +17,7 @@ class IncrementScriptVersion(pyblish.api.ContextPlugin): assert all(result["success"] for result in context.data["results"]), ( "Publishing not succesfull so version is not increased.") - instances = context[:] - - prerender_check = list() - families_check = list() - for instance in instances: - if ("prerender" in str(instance)) and instance.data.get("families", None): - prerender_check.append(instance) - if instance.data.get("families", None): - families_check.append(True) - - - if len(prerender_check) != len(families_check): - from pype.lib import version_up - path = context.data["currentFile"] - nuke.scriptSaveAs(version_up(path)) - self.log.info('Incrementing script version') + from pype.lib import version_up + path = context.data["currentFile"] + nuke.scriptSaveAs(version_up(path)) + self.log.info('Incrementing script version') diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py index 6e9b91dd72..425789f18a 100644 --- a/pype/plugins/nuke/publish/validate_rendered_frames.py +++ b/pype/plugins/nuke/publish/validate_rendered_frames.py @@ -28,7 +28,7 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): """ Validates file output. """ order = pyblish.api.ValidatorOrder + 0.1 - families = ["render"] + families = ["render", "prerender"] label = "Validate rendered frame" hosts = ["nuke", "nukestudio"] From f25dea58a808e80876acf5115c41a7fd2c2b3272 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 18:57:20 +0100 Subject: [PATCH 173/327] fix(nuke, global): shuffling families after plugins finish processing --- pype/plugins/nuke/publish/collect_writes.py | 24 ++++++++++++++----- .../nuke/publish/extract_render_local.py | 18 ++++++++++---- .../nuke/publish/submit_nuke_deadline.py | 10 ++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index f3f33b7a6d..6379a1db87 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -12,9 +12,11 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): hosts = ["nuke", "nukeassist"] families = ["write"] + # preset attributes + sync_workfile_version = True + def process(self, instance): - # adding 2d focused rendering - instance.data["families"].append("render2d") + families = instance.data["families"] node = None for x in instance: @@ -52,10 +54,13 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): output_dir = os.path.dirname(path) self.log.debug('output dir: {}'.format(output_dir)) - # get version to instance for integration - instance.data['version'] = instance.context.data["version"] + if not next((f for f in families + if "prerender" in f), + None) and self.sync_workfile_version: + # get version to instance for integration + instance.data['version'] = instance.context.data["version"] - self.log.debug('Write Version: %s' % instance.data('version')) + self.log.debug('Write Version: %s' % instance.data('version')) # create label name = node.name() @@ -66,7 +71,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): int(last_frame) ) - if [fm for fm in instance.data['families'] + if [fm for fm in families if fm in ["render", "prerender"]]: if "representations" not in instance.data: instance.data["representations"] = list() @@ -145,6 +150,13 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) + + if "prerender" in families: + instance.data.update({ + "family": "prerender", + "families": [] + }) + self.log.debug("families: {}".format(families)) self.log.debug("instance.data: {}".format(instance.data)) diff --git a/pype/plugins/nuke/publish/extract_render_local.py b/pype/plugins/nuke/publish/extract_render_local.py index 1dad413ee5..b7aa59a457 100644 --- a/pype/plugins/nuke/publish/extract_render_local.py +++ b/pype/plugins/nuke/publish/extract_render_local.py @@ -20,6 +20,8 @@ class NukeRenderLocal(pype.api.Extractor): families = ["render.local", "prerender.local"] def process(self, instance): + families = instance.data["families"] + node = None for x in instance: if x.Class() == "Write": @@ -30,7 +32,7 @@ class NukeRenderLocal(pype.api.Extractor): first_frame = instance.data.get("frameStartHandle", None) # exception for slate workflow - if "slate" in instance.data["families"]: + if "slate" in families: first_frame -= 1 last_frame = instance.data.get("frameEndHandle", None) @@ -53,7 +55,7 @@ class NukeRenderLocal(pype.api.Extractor): ) # exception for slate workflow - if "slate" in instance.data["families"]: + if "slate" in families: first_frame += 1 path = node['file'].value() @@ -79,8 +81,16 @@ class NukeRenderLocal(pype.api.Extractor): out_dir )) - instance.data['family'] = 'render' - instance.data['families'].append('render') + # redefinition of families + if "render.local" in families: + instance.data['family'] = 'render2d' + families.remove('render.local') + families.insert(0, "render") + elif "prerender.local" in families: + instance.data['family'] = 'prerender' + families.remove('prerender.local') + families.insert(0, "prerender") + instance.data["families"] = families collections, remainder = clique.assemble(collected_frames) self.log.info('collections: {}'.format(str(collections))) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 3da2e58e4d..7990c20112 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -28,6 +28,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_chunk_size = 1 def process(self, instance): + families = instance.data["families"] node = instance[0] context = instance.context @@ -82,6 +83,15 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): instance.data["deadlineSubmissionJob"] = resp.json() instance.data["publishJobState"] = "Suspended" + # redefinition of families + if "render.farm" in families: + instance.data['family'] = 'write' + families.insert(0, "render2d") + elif "prerender.farm" in families: + instance.data['family'] = 'write' + families.insert(0, "prerender") + instance.data["families"] = families + def payload_submit(self, instance, script_path, From 93761e3010743d139f272022a578af3d90e4acfc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 18:57:52 +0100 Subject: [PATCH 174/327] fix(global): add correct frame number to repre.data --- pype/plugins/global/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 0ceac1f4a7..44f91a343c 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -383,6 +383,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not dst_start_frame: dst_start_frame = dst_padding + template_data["frame"] = dst_start_frame + dst = "{0}{1}{2}".format( dst_head, dst_start_frame, From 5f616f14f1a97db551b713a1ab73415d9ab6ecd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 18:58:18 +0100 Subject: [PATCH 175/327] fix(nuke): submitting with correct family --- .../global/publish/submit_publish_job.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 144a0822a2..134f8e9098 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -141,7 +141,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): hosts = ["fusion", "maya", "nuke"] - families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence"] + families = ["render.farm", "prerener", "renderlayer", "imagesequence"] aov_filter = {"maya": ["beauty"]} @@ -168,9 +168,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instance_transfer = { "slate": ["slateFrame"], "review": ["lutPath"], - "render.farm": ["bakeScriptPath", "bakeRenderPath", - "bakeWriteNodeName", "version"] - } + "render2d": ["bakeScriptPath", "bakeRenderPath", + "bakeWriteNodeName", "version"] + } # list of family names to transfer to new family if present families_transfer = ["render3d", "render2d", "ftrack", "slate"] @@ -586,17 +586,24 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionHeight": data.get("resolutionHeight", 1080), } - if "prerender.farm" in instance.data["families"]: + if "prerender" in instance.data["families"]: instance_skeleton_data.update({ "family": "prerender", - "families": ["prerender"] - }) + "families": [] + }) # transfer specific families from original instance to new render for item in self.families_transfer: if item in instance.data.get("families", []): instance_skeleton_data["families"] += [item] + if "render.farm" in instance.data["families"]: + instance_skeleton_data.update({ + "family": "render2d", + "families": ["render"] + [f for f in instance.data["families"] + if "render.farm" not in f] + }) + # transfer specific properties from original instance based on # mapping dictionary `instance_transfer` for key, values in self.instance_transfer.items(): From 129eb5db6931832c8a3592295e352dbdd4700ea9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 19:04:03 +0100 Subject: [PATCH 176/327] fix(nuke): hound pep8 --- pype/plugins/nuke/create/create_write_prerender.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pype/plugins/nuke/create/create_write_prerender.py b/pype/plugins/nuke/create/create_write_prerender.py index 6e242f886c..210c84e0cd 100644 --- a/pype/plugins/nuke/create/create_write_prerender.py +++ b/pype/plugins/nuke/create/create_write_prerender.py @@ -1,8 +1,7 @@ from collections import OrderedDict from pype.nuke import ( plugin, - lib as pnlib - ) + lib as pnlib) import nuke @@ -81,15 +80,15 @@ class CreateWritePrerender(plugin.PypeCreator): else: self.log.info("Adding template path from plugin") write_data.update({ - "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}"}) + "fpath_template": ("{work}/prerenders/nuke/{subset}" + "/{subset}.{frame}.{ext}")}) write_node = pnlib.create_write_node( self.data["subset"], write_data, input=selected_node, prenodes=[], - review=False - ) + review=False) # relinking to collected connections for i, input in enumerate(inputs): From 8d9d965d6d4f8b6fa8a217e45415f3eee611f866 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 19:06:19 +0100 Subject: [PATCH 177/327] fix(nuke): hound pep8 --- pype/plugins/nuke/create/create_write_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/nuke/create/create_write_render.py b/pype/plugins/nuke/create/create_write_render.py index c3b60ba2b0..06ef237305 100644 --- a/pype/plugins/nuke/create/create_write_render.py +++ b/pype/plugins/nuke/create/create_write_render.py @@ -1,8 +1,7 @@ from collections import OrderedDict from pype.nuke import ( plugin, - lib as pnlib - ) + lib as pnlib) import nuke @@ -82,7 +81,8 @@ class CreateWriteRender(plugin.PypeCreator): else: self.log.info("Adding template path from plugin") write_data.update({ - "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}"}) + "fpath_template": ("{work}/renders/nuke/{subset}" + "/{subset}.{frame}.{ext}")}) write_node = pnlib.create_write_node( self.data["subset"], From 48d7489621f36fe6743207747f093df43f8512f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 19:12:34 +0100 Subject: [PATCH 178/327] fix(global): hound pep8 --- .../global/publish/extract_review_slate.py | 15 +++++++++------ pype/plugins/global/publish/submit_publish_job.py | 5 ++--- .../nuke/publish/extract_review_data_mov.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/extract_review_slate.py b/pype/plugins/global/publish/extract_review_slate.py index da94c7714a..aaa67bde68 100644 --- a/pype/plugins/global/publish/extract_review_slate.py +++ b/pype/plugins/global/publish/extract_review_slate.py @@ -36,7 +36,8 @@ class ExtractReviewSlate(pype.api.Extractor): fps = inst_data.get("fps") # defining image ratios - resolution_ratio = (float(resolution_width) * pixel_aspect) / resolution_height + resolution_ratio = ((float(resolution_width) * pixel_aspect) / + resolution_height) delivery_ratio = float(to_width) / float(to_height) self.log.debug("__ resolution_ratio: `{}`".format(resolution_ratio)) self.log.debug("__ delivery_ratio: `{}`".format(delivery_ratio)) @@ -91,7 +92,7 @@ class ExtractReviewSlate(pype.api.Extractor): input_args.extend([ "-r {}".format(fps), "-t 0.04"] - ) + ) # output args codec_args = repre["_profile"].get('codec', []) @@ -113,7 +114,7 @@ class ExtractReviewSlate(pype.api.Extractor): self.log.debug("lower then delivery") width_scale = int(to_width * scale_factor) width_half_pad = int(( - to_width - width_scale)/2) + to_width - width_scale) / 2) height_scale = to_height height_half_pad = 0 else: @@ -126,7 +127,7 @@ class ExtractReviewSlate(pype.api.Extractor): height_scale = int( resolution_height * scale_factor) height_half_pad = int( - (to_height - height_scale)/2) + (to_height - height_scale) / 2) self.log.debug( "__ width_scale: `{}`".format(width_scale)) @@ -137,8 +138,10 @@ class ExtractReviewSlate(pype.api.Extractor): self.log.debug( "__ height_half_pad: `{}`".format(height_half_pad)) - scaling_arg = "scale={0}x{1}:flags=lanczos,pad={2}:{3}:{4}:{5}:black,setsar=1".format( - width_scale, height_scale, to_width, to_height, width_half_pad, height_half_pad + scaling_arg = ("scale={0}x{1}:flags=lanczos," + "pad={2}:{3}:{4}:{5}:black,setsar=1").format( + width_scale, height_scale, to_width, to_height, + width_half_pad, height_half_pad ) vf_back = self.add_video_filter_args( diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 134f8e9098..af16b11db6 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -170,7 +170,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "review": ["lutPath"], "render2d": ["bakeScriptPath", "bakeRenderPath", "bakeWriteNodeName", "version"] - } + } # list of family names to transfer to new family if present families_transfer = ["render3d", "render2d", "ftrack", "slate"] @@ -589,8 +589,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if "prerender" in instance.data["families"]: instance_skeleton_data.update({ "family": "prerender", - "families": [] - }) + "families": []}) # transfer specific families from original instance to new render for item in self.families_transfer: diff --git a/pype/plugins/nuke/publish/extract_review_data_mov.py b/pype/plugins/nuke/publish/extract_review_data_mov.py index 0bd5394548..7c56dc8b92 100644 --- a/pype/plugins/nuke/publish/extract_review_data_mov.py +++ b/pype/plugins/nuke/publish/extract_review_data_mov.py @@ -3,7 +3,7 @@ import pyblish.api from avalon.nuke import lib as anlib from pype.nuke import lib as pnlib import pype -reload(pnlib) + class ExtractReviewDataMov(pype.api.Extractor): """Extracts movie and thumbnail with baked in luts From 599a227359e1bc57271215441af8e22f957bdd90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 13:18:26 +0100 Subject: [PATCH 179/327] fix queue import --- pype/scripts/slates/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/lib.py index a73f87e82f..154c689349 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/lib.py @@ -1,5 +1,9 @@ import logging -from queue import Queue + +try: + from queue import Queue +except Exception: + from Queue import Queue from .slate_base.main_frame import MainFrame from .slate_base.layer import Layer From 9ca3541c827353f4da8656853f6469f2ea7ab320 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 15:30:46 +0100 Subject: [PATCH 180/327] slates are more like package, fixed few bugs and example is in specific file with more data of example --- pype/scripts/slates/__init__.py | 2 + pype/scripts/slates/__main__.py | 10 + pype/scripts/slates/slate_base/api.py | 15 ++ pype/scripts/slates/slate_base/example.py | 254 ++++++++++++++++++++ pype/scripts/slates/slate_base/items.py | 1 + pype/scripts/slates/{ => slate_base}/lib.py | 69 ++---- 6 files changed, 300 insertions(+), 51 deletions(-) create mode 100644 pype/scripts/slates/__init__.py create mode 100644 pype/scripts/slates/__main__.py create mode 100644 pype/scripts/slates/slate_base/api.py create mode 100644 pype/scripts/slates/slate_base/example.py rename pype/scripts/slates/{ => slate_base}/lib.py (63%) diff --git a/pype/scripts/slates/__init__.py b/pype/scripts/slates/__init__.py new file mode 100644 index 0000000000..52937708ea --- /dev/null +++ b/pype/scripts/slates/__init__.py @@ -0,0 +1,2 @@ +from . import slate_base +from .slate_base import api diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py new file mode 100644 index 0000000000..29282d3226 --- /dev/null +++ b/pype/scripts/slates/__main__.py @@ -0,0 +1,10 @@ +from slate_base import api + + +def main(in_args=None): + # TODO proper argument handling + api.example() + + +if __name__ == "__main__": + main() diff --git a/pype/scripts/slates/slate_base/api.py b/pype/scripts/slates/slate_base/api.py new file mode 100644 index 0000000000..cd64c68134 --- /dev/null +++ b/pype/scripts/slates/slate_base/api.py @@ -0,0 +1,15 @@ +from .font_factory import FontFactory +from .base import BaseObj, load_default_style +from .main_frame import MainFrame +from .layer import Layer +from .items import ( + BaseItem, + ItemImage, + ItemRectangle, + ItemPlaceHolder, + ItemText, + ItemTable, + TableField +) +from .lib import create_slates +from .example import example diff --git a/pype/scripts/slates/slate_base/example.py b/pype/scripts/slates/slate_base/example.py new file mode 100644 index 0000000000..560f9ec02d --- /dev/null +++ b/pype/scripts/slates/slate_base/example.py @@ -0,0 +1,254 @@ +# import sys +# sys.append(r"PATH/TO/PILLOW/PACKAGE") + +from . import api + + +def example(): + """Example data to demontrate function. + + It is required to fill "destination_path", "thumbnail_path" + and "color_bar_path" in `example_fill_data` to be able to execute. + """ + + example_fill_data = { + "destination_path": "PATH/TO/OUTPUT/FILE", + "project": { + "name": "Testing project" + }, + "intent": "WIP", + "version_name": "seq01_sh0100_compositing_v01", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." + " Aenean commodo ligula eget dolor. Aenean massa." + " Cum sociis natoque penatibus et magnis dis parturient montes," + " nascetur ridiculus mus. Donec quam felis, ultricies nec," + " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" + " enim. Donec pede justo, fringilla vel," + " aliquet nec, vulputate eget, arcu." + ), + "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", + "color_bar_path": "PATH/TO/COLOR/BAR/FILE", + "vendor": "Our Studio", + "shot_name": "sh0100", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } + + example_presets = {"example_HD": { + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "font-family": "arial", + "font-color": "#ffffff", + "font-bold": False, + "font-italic": False, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": True + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": False, + "font-italic": False, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": False, + "ellide": True, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": True, + "ellide": False, + "word-wrap": True, + "max-lines": None + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": True, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[0:1]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[0:1]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": True + } + }, { + "type": "table", + "use_alternate_color": True, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[0:1]": { + "font-bold": True + }, + "table-item-field[3:0]": { + "word-wrap": True, + "ellide": True, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": True, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": True, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] + }} + + api.create_slates(example_fill_data, "example_HD", example_presets) diff --git a/pype/scripts/slates/slate_base/items.py b/pype/scripts/slates/slate_base/items.py index ea31443f80..1183d73305 100644 --- a/pype/scripts/slates/slate_base/items.py +++ b/pype/scripts/slates/slate_base/items.py @@ -1,3 +1,4 @@ +import os import re from PIL import Image diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/slate_base/lib.py similarity index 63% rename from pype/scripts/slates/lib.py rename to pype/scripts/slates/slate_base/lib.py index 154c689349..3c7a465e98 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/slate_base/lib.py @@ -1,17 +1,19 @@ import logging - try: from queue import Queue except Exception: from Queue import Queue -from .slate_base.main_frame import MainFrame -from .slate_base.layer import Layer -from .slate_base.items import ( +from .main_frame import MainFrame +from .layer import Layer +from .items import ( ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder ) -from pypeapp import config +try: + from pypeapp.config import get_presets +except Exception: + get_presets = dict log = logging.getLogger(__name__) @@ -19,17 +21,20 @@ log = logging.getLogger(__name__) RequiredSlateKeys = ["width", "height", "destination_path"] -def create_slates(fill_data, slate_name): - presets = config.get_presets() - slate_presets = ( - presets - .get("tools", {}) - .get("slates") - ) or {} +def create_slates(fill_data, slate_name, slate_presets=None): + if slate_presets is None: + presets = get_presets() + slate_presets = ( + presets + .get("tools", {}) + .get("slates") + ) or {} slate_data = slate_presets.get(slate_name) if not slate_data: - log.error("Slate data of <{}> does not exists.") + log.error( + "Name \"{}\" was not found in slate presets.".format(slate_name) + ) return False missing_keys = [] @@ -111,41 +116,3 @@ def create_slates(fill_data, slate_name): main.draw() log.debug("Slate creation finished") - - -def example(): - # import sys - # sys.append(r"PATH/TO/PILLOW/PACKAGE") - # sys.append(r"PATH/TO/PYPE-SETUP") - - fill_data = { - "destination_path": "PATH/TO/OUTPUT/FILE", - "project": { - "name": "Testing project" - }, - "intent": "WIP", - "version_name": "seq01_sh0100_compositing_v01", - "date": "2019-08-09", - "shot_type": "2d comp", - "submission_note": ( - "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." - " Aenean commodo ligula eget dolor. Aenean massa." - " Cum sociis natoque penatibus et magnis dis parturient montes," - " nascetur ridiculus mus. Donec quam felis, ultricies nec," - " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" - " enim. Donec pede justo, fringilla vel," - " aliquet nec, vulputate eget, arcu." - ), - "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", - "color_bar_path": "PATH/TO/COLOR/BAR/FILE", - "vendor": "Our Studio", - "shot_name": "sh0100", - "frame_start": 1001, - "frame_end": 1004, - "duration": 3 - } - create_slates(fill_data, "example_HD") - - -if __name__ == "__main__": - example() From f152b9b6a7c691d962344519be36cf1d12b84b6f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 Mar 2020 16:19:08 +0100 Subject: [PATCH 181/327] do not allow token in Redshift --- .../maya/publish/validate_rendersettings.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/publish/validate_rendersettings.py b/pype/plugins/maya/publish/validate_rendersettings.py index c98f0f8cdc..67239d4790 100644 --- a/pype/plugins/maya/publish/validate_rendersettings.py +++ b/pype/plugins/maya/publish/validate_rendersettings.py @@ -13,13 +13,17 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): """Validates the global render settings * File Name Prefix must start with: `maya/` - all other token are customizable but sane values are: + all other token are customizable but sane values for Arnold are: `maya///_` - token is supported also, usefull for multiple renderable + token is supported also, useful for multiple renderable cameras per render layer. + For Redshift omit token. Redshift will append it + automatically if AOVs are enabled and if you user Multipart EXR + it doesn't make much sense. + * Frame Padding must be: * default: 4 @@ -127,8 +131,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): # no vray checks implemented yet pass elif renderer == "redshift": - # no redshift check implemented yet - pass + if re.search(cls.R_AOV_TOKEN, prefix): + invalid = True + cls.log.error("Do not use AOV token [ {} ] - " + "Redshift automatically append AOV name and " + "it doesn't make much sense with " + "Multipart EXR".format(prefix)) + elif renderer == "renderman": file_prefix = cmds.getAttr("rmanGlobals.imageFileFormat") dir_prefix = cmds.getAttr("rmanGlobals.imageOutputDir") @@ -143,8 +152,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): dir_prefix)) else: - multichannel = cmds.getAttr("defaultArnoldDriver.mergeAOVs") - if multichannel: + multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs") + if multipart: if re.search(cls.R_AOV_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " From a5d313492f7f157513c124894289f297a7ec237c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 16:27:52 +0100 Subject: [PATCH 182/327] hopefully fix creating items --- pype/scripts/slates/slate_base/items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/scripts/slates/slate_base/items.py b/pype/scripts/slates/slate_base/items.py index 1183d73305..6d19fc6a0c 100644 --- a/pype/scripts/slates/slate_base/items.py +++ b/pype/scripts/slates/slate_base/items.py @@ -36,8 +36,8 @@ class ItemImage(BaseItem): obj_type = "image" def __init__(self, image_path, *args, **kwargs): - super(ItemImage, self).__init__(*args, **kwargs) self.image_path = image_path + super(ItemImage, self).__init__(*args, **kwargs) def fill_data_format(self): if re.match(self.fill_data_regex, self.image_path): @@ -143,8 +143,8 @@ class ItemText(BaseItem): obj_type = "text" def __init__(self, value, *args, **kwargs): - super(ItemText, self).__init__(*args, **kwargs) self.value = value + super(ItemText, self).__init__(*args, **kwargs) def draw(self, image, drawer): bg_color = self.style["bg-color"] From 41e6eb05caf8bf053bc2d8183a484be278436f20 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Mar 2020 17:06:00 +0100 Subject: [PATCH 183/327] fix(nuke): build first workfile was not accepting jpeg sequences --- pype/nuke/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index ad2d576da3..3ff9c6d397 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1135,7 +1135,7 @@ class BuildWorkfile(WorkfileSettings): regex_filter=None, version=None, representations=["exr", "dpx", "lutJson", "mov", - "preview", "png"]): + "preview", "png", "jpeg", "jpg"]): """ A short description. From 3e216dc3b39026515aa8ff2d86b25224ce68bb8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Mar 2020 12:04:00 +0100 Subject: [PATCH 184/327] slates can receive data though command line like burnins and it is possible to save slates metadata to json --- pype/scripts/slates/__main__.py | 13 +++++-- pype/scripts/slates/slate_base/lib.py | 56 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py index 29282d3226..43efcfbb06 100644 --- a/pype/scripts/slates/__main__.py +++ b/pype/scripts/slates/__main__.py @@ -1,10 +1,17 @@ +import json from slate_base import api def main(in_args=None): - # TODO proper argument handling - api.example() + data_arg = in_args[-1] + in_data = json.loads(data_arg) + api.create_slates( + in_data["fill_data"], + in_data.get("slate_name"), + in_data.get("slate_data"), + in_data.get("data_output_json") + ) if __name__ == "__main__": - main() + main(sys.argv) diff --git a/pype/scripts/slates/slate_base/lib.py b/pype/scripts/slates/slate_base/lib.py index 3c7a465e98..d9f8ad6d42 100644 --- a/pype/scripts/slates/slate_base/lib.py +++ b/pype/scripts/slates/slate_base/lib.py @@ -1,3 +1,5 @@ +import os +import json import logging try: from queue import Queue @@ -21,21 +23,36 @@ log = logging.getLogger(__name__) RequiredSlateKeys = ["width", "height", "destination_path"] -def create_slates(fill_data, slate_name, slate_presets=None): - if slate_presets is None: - presets = get_presets() +# TODO proper documentation +def create_slates( + fill_data, slate_name=None, slate_data=None, data_output_json=None +): + """Implmentation for command line executing. + + Data for slates are by defaule taken from presets. That requires to enter, + `slate_name`. If `slate_data` are entered then they are used. + + `data_output` should be path to json file where data will be collected. + """ + if slate_data is None and slate_name is None: + raise TypeError( + "`create_slates` expects to enter data for slates or name" + " of slate preset." + ) + + elif slate_data is None: slate_presets = ( - presets + get_presets() .get("tools", {}) .get("slates") ) or {} - slate_data = slate_presets.get(slate_name) - - if not slate_data: - log.error( - "Name \"{}\" was not found in slate presets.".format(slate_name) - ) - return False + slate_data = slate_presets.get(slate_name) + if slate_data is None: + raise ValueError( + "Preset name \"{}\" was not found in slate presets.".format( + slate_name + ) + ) missing_keys = [] for key in RequiredSlateKeys: @@ -116,3 +133,20 @@ def create_slates(fill_data, slate_name, slate_presets=None): main.draw() log.debug("Slate creation finished") + + if not data_output_json: + return + + if not data_output_json.endswith(".json"): + raise ValueError("Output path must be .json file.") + + data_output_json_dir = os.path.dirname(data_output_json) + if not os.path.exists(data_output_json_dir): + log.info("Creating folder \"{}\"".format(data_output_json_dir)) + os.makedirs(data_output_json_dir) + + output_data = main.collect_data() + with open(data_output_json, "w") as json_file: + json_file.write(json.dumps(output_data, indent=4)) + + log.info("Metadata collected in \"{}\".".format(data_output_json)) From 9a749a03a950afbcccb8467f6308f0d7456de4ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Mar 2020 12:06:04 +0100 Subject: [PATCH 185/327] added important import --- pype/scripts/slates/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py index 43efcfbb06..bd49389d84 100644 --- a/pype/scripts/slates/__main__.py +++ b/pype/scripts/slates/__main__.py @@ -1,3 +1,4 @@ +import sys import json from slate_base import api From 46cc7b2b00657f8fd10fcc503ff03f144ce7843f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 27 Mar 2020 17:08:04 +0100 Subject: [PATCH 186/327] disable ffmpeg for multipart EXRs --- pype/plugins/global/publish/extract_burnin.py | 4 + pype/plugins/global/publish/extract_jpeg.py | 4 + pype/plugins/global/publish/extract_review.py | 4 + .../global/publish/submit_publish_job.py | 4 +- pype/plugins/maya/publish/collect_render.py | 409 ++++++++++-------- 5 files changed, 239 insertions(+), 186 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 086a1fdfb2..9ef97e19d0 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -65,6 +65,10 @@ class ExtractBurnin(pype.api.Extractor): for i, repre in enumerate(instance.data["representations"]): self.log.debug("__ i: `{}`, repre: `{}`".format(i, repre)) + if repre.get("multipartExr", False) is True: + # ffmpeg doesn't support multipart exrs + continue + if "burnin" not in repre.get("tags", []): continue diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 9ad6a15dfe..c856007d31 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -36,6 +36,10 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if not isinstance(repre['files'], list): continue + if repre.get("multipartExr", False) is True: + # ffmpeg doesn't support multipart exrs + continue + stagingdir = os.path.normpath(repre.get("stagingDir")) input_file = repre['files'][0] diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index c8a8510fb2..02504edd14 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -57,6 +57,10 @@ class ExtractReview(pyblish.api.InstancePlugin): # filter out mov and img sequences representations_new = representations[:] for repre in representations: + if repre.get("multipartExr", False) is True: + # ffmpeg doesn't support multipart exrs + continue + if repre['ext'] not in self.ext_filter: continue diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index dcf19ae32c..6c88b5d2aa 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -443,6 +443,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "stagingDir": os.path.dirname(list(c)[0]), "anatomy_template": "render", "fps": instance.get("fps"), + "multipartExr": instance.get("multipartExr"), "tags": ["review", "preview"] if preview else [], } @@ -458,7 +459,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "ext": ext, "files": os.path.basename(r), "stagingDir": os.path.dirname(r), - "anatomy_template": "publish", + "anatomy_template": "publish" } if r in bake_render_path: rep.update({ @@ -581,6 +582,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "pixelAspect": data.get("pixelAspect", 1), "resolutionWidth": data.get("resolutionWidth", 1920), "resolutionHeight": data.get("resolutionHeight", 1080), + "multipartExr": data.get("multipartExr", False) } # transfer specific families from original instance to new render diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index be3878e6bd..ac21d9ad03 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -52,38 +52,40 @@ from avalon import maya, api import pype.maya.lib as lib -R_SINGLE_FRAME = re.compile(r'^(-?)\d+$') -R_FRAME_RANGE = re.compile(r'^(?P(-?)\d+)-(?P(-?)\d+)$') -R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') +R_SINGLE_FRAME = re.compile(r"^(-?)\d+$") +R_FRAME_RANGE = re.compile(r"^(?P(-?)\d+)-(?P(-?)\d+)$") +R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") R_LAYER_TOKEN = re.compile( - r'.*%l.*|.*.*|.*.*', re.IGNORECASE) -R_AOV_TOKEN = re.compile(r'.*%a.*|.*.*|.*.*', re.IGNORECASE) -R_SUBSTITUTE_AOV_TOKEN = re.compile(r'%a||', re.IGNORECASE) -R_REMOVE_AOV_TOKEN = re.compile(r'_%a|_|_', re.IGNORECASE) + r".*%l.*|.*.*|.*.*", re.IGNORECASE +) +R_AOV_TOKEN = re.compile(r".*%a.*|.*.*|.*.*", re.IGNORECASE) +R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a||", re.IGNORECASE) +R_REMOVE_AOV_TOKEN = re.compile(r"_%a|_|_", re.IGNORECASE) # to remove unused renderman tokens -R_CLEAN_FRAME_TOKEN = re.compile(r'\.?\.?', re.IGNORECASE) -R_CLEAN_EXT_TOKEN = re.compile(r'\.?\.?', re.IGNORECASE) +R_CLEAN_FRAME_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) +R_CLEAN_EXT_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) R_SUBSTITUTE_LAYER_TOKEN = re.compile( - r'%l||', re.IGNORECASE) -R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r'%c|', re.IGNORECASE) -R_SUBSTITUTE_SCENE_TOKEN = re.compile(r'%s|', re.IGNORECASE) + r"%l||", re.IGNORECASE +) +R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c|", re.IGNORECASE) +R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s|", re.IGNORECASE) RENDERER_NAMES = { - 'mentalray': 'MentalRay', - 'vray': 'V-Ray', - 'arnold': 'Arnold', - 'renderman': 'Renderman', - 'redshift': 'Redshift' + "mentalray": "MentalRay", + "vray": "V-Ray", + "arnold": "Arnold", + "renderman": "Renderman", + "redshift": "Redshift", } # not sure about the renderman image prefix ImagePrefixes = { - 'mentalray': 'defaultRenderGlobals.imageFilePrefix', - 'vray': 'vraySettings.fileNamePrefix', - 'arnold': 'defaultRenderGlobals.imageFilePrefix', - 'renderman': 'rmanGlobals.imageFileFormat', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + "mentalray": "defaultRenderGlobals.imageFilePrefix", + "vray": "vraySettings.fileNamePrefix", + "arnold": "defaultRenderGlobals.imageFilePrefix", + "renderman": "rmanGlobals.imageFileFormat", + "redshift": "defaultRenderGlobals.imageFilePrefix", } @@ -97,21 +99,23 @@ class CollectMayaRender(pyblish.api.ContextPlugin): def process(self, context): render_instance = None for instance in context: - if 'rendering' in instance.data['families']: + if "rendering" in instance.data["families"]: render_instance = instance render_instance.data["remove"] = True # make sure workfile instance publishing is enabled - if 'workfile' in instance.data['families']: + if "workfile" in instance.data["families"]: instance.data["publish"] = True if not render_instance: - self.log.info("No render instance found, skipping render " - "layer collection.") + self.log.info( + "No render instance found, skipping render " + "layer collection." + ) return render_globals = render_instance - collected_render_layers = render_instance.data['setMembers'] + collected_render_layers = render_instance.data["setMembers"] filepath = context.data["currentFile"].replace("\\", "/") asset = api.Session["AVALON_ASSET"] workspace = context.data["workspaceDir"] @@ -126,22 +130,24 @@ class CollectMayaRender(pyblish.api.ContextPlugin): try: expected_layer_name = re.search(r"^LAYER_(.*)", layer).group(1) except IndexError: - msg = ("Invalid layer name in set [ {} ]".format(layer)) + msg = "Invalid layer name in set [ {} ]".format(layer) self.log.warnig(msg) continue self.log.info("processing %s" % layer) # check if layer is part of renderSetup if expected_layer_name not in maya_render_layers: - msg = ("Render layer [ {} ] is not in " - "Render Setup".format(expected_layer_name)) + msg = "Render layer [ {} ] is not in " "Render Setup".format( + expected_layer_name + ) self.log.warning(msg) continue # check if layer is renderable if not maya_render_layers[expected_layer_name].isRenderable(): - msg = ("Render layer [ {} ] is not " - "renderable".format(expected_layer_name)) + msg = "Render layer [ {} ] is not " "renderable".format( + expected_layer_name + ) self.log.warning(msg) continue @@ -150,26 +156,31 @@ class CollectMayaRender(pyblish.api.ContextPlugin): attachTo = [] if sets: for s in sets: - attachTo.append({ - "version": None, # we need integrator to get version - "subset": s, - "family": cmds.getAttr("{}.family".format(s)) - }) + attachTo.append( + { + "version": None, # we need integrator for that + "subset": s, + "family": cmds.getAttr("{}.family".format(s)), + } + ) self.log.info(" -> attach render to: {}".format(s)) layer_name = "rs_{}".format(expected_layer_name) # collect all frames we are expecting to be rendered renderer = cmds.getAttr( - 'defaultRenderGlobals.currentRenderer').lower() + "defaultRenderGlobals.currentRenderer" + ).lower() # handle various renderman names - if renderer.startswith('renderman'): - renderer = 'renderman' + if renderer.startswith("renderman"): + renderer = "renderman" # return all expected files for all cameras and aovs in given # frame range - exp_files = ExpectedFiles().get(renderer, layer_name) - assert exp_files, ("no file names were generated, this is bug") + exf = ExpectedFiles() + exp_files = exf.get(renderer, layer_name) + self.log.info("multipart: {}".format(exf.multipart)) + assert exp_files, "no file names were generated, this is bug" # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV @@ -177,7 +188,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if attachTo: assert len(exp_files[0].keys()) == 1, ( "attaching multiple AOVs or renderable cameras to " - "subset is not supported") + "subset is not supported" + ) # append full path full_exp_files = [] @@ -210,35 +222,45 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "subset": expected_layer_name, "attachTo": attachTo, "setMembers": layer_name, + "multipartExr": exf.multipart, "publish": True, - "frameStart": int(context.data["assetEntity"]['data']['frameStart']), - "frameEnd": int(context.data["assetEntity"]['data']['frameEnd']), - "frameStartHandle": int(self.get_render_attribute("startFrame", - layer=layer_name)), - "frameEndHandle": int(self.get_render_attribute("endFrame", - layer=layer_name)), + "frameStart": int( + context.data["assetEntity"]["data"]["frameStart"] + ), + "frameEnd": int( + context.data["assetEntity"]["data"]["frameEnd"] + ), + "frameStartHandle": int( + self.get_render_attribute("startFrame", layer=layer_name) + ), + "frameEndHandle": int( + self.get_render_attribute("endFrame", layer=layer_name) + ), "byFrameStep": int( - self.get_render_attribute("byFrameStep", - layer=layer_name)), - "renderer": self.get_render_attribute("currentRenderer", - layer=layer_name), - "handleStart": int(context.data["assetEntity"]['data']['handleStart']), - "handleEnd": int(context.data["assetEntity"]['data']['handleEnd']), - + self.get_render_attribute("byFrameStep", layer=layer_name) + ), + "renderer": self.get_render_attribute( + "currentRenderer", layer=layer_name + ), + "handleStart": int( + context.data["assetEntity"]["data"]["handleStart"] + ), + "handleEnd": int( + context.data["assetEntity"]["data"]["handleEnd"] + ), # instance subset "family": "renderlayer", "families": ["renderlayer"], "asset": asset, "time": api.time(), "author": context.data["user"], - # Add source to allow tracing back to the scene from # which was submitted originally "source": filepath, "expectedFiles": full_exp_files, "resolutionWidth": cmds.getAttr("defaultResolution.width"), "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect") + "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), } # Apply each user defined attribute as data @@ -261,8 +283,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # Define nice label label = "{0} ({1})".format(expected_layer_name, data["asset"]) - label += " [{0}-{1}]".format(int(data["frameStartHandle"]), - int(data["frameEndHandle"])) + label += " [{0}-{1}]".format( + int(data["frameStartHandle"]), int(data["frameEndHandle"]) + ) instance = context.create_instance(expected_layer_name) instance.data["label"] = label @@ -298,7 +321,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): machine_list = attributes["machineList"] if machine_list: key = "Whitelist" if attributes["whitelist"] else "Blacklist" - options['renderGlobals'][key] = machine_list + options["renderGlobals"][key] = machine_list # Suspend publish job state = "Suspended" if attributes["suspendPublishJob"] else "Active" @@ -354,32 +377,41 @@ class CollectMayaRender(pyblish.api.ContextPlugin): return rset.getOverrides() def get_render_attribute(self, attr, layer): - return lib.get_attr_in_layer("defaultRenderGlobals.{}".format(attr), - layer=layer) + return lib.get_attr_in_layer( + "defaultRenderGlobals.{}".format(attr), layer=layer + ) class ExpectedFiles: + multipart = False def get(self, renderer, layer): - if renderer.lower() == 'arnold': - return ExpectedFilesArnold(layer).get_files() - elif renderer.lower() == 'vray': - return ExpectedFilesVray(layer).get_files() - elif renderer.lower() == 'redshift': - return ExpectedFilesRedshift(layer).get_files() - elif renderer.lower() == 'mentalray': - return ExpectedFilesMentalray(layer).get_files() - elif renderer.lower() == 'renderman': - return ExpectedFilesRenderman(layer).get_files() + if renderer.lower() == "arnold": + return self._get_files(ExpectedFilesArnold(layer)) + elif renderer.lower() == "vray": + return self._get_files(ExpectedFilesVray(layer)) + elif renderer.lower() == "redshift": + return self._get_files(ExpectedFilesRedshift(layer)) + elif renderer.lower() == "mentalray": + return self._get_files(ExpectedFilesMentalray(layer)) + elif renderer.lower() == "renderman": + return self._get_files(ExpectedFilesRenderman(layer)) else: raise UnsupportedRendererException( - "unsupported {}".format(renderer)) + "unsupported {}".format(renderer) + ) + + def _get_files(self, renderer): + files = renderer.get_files() + self.multipart = renderer.multipart + return files @six.add_metaclass(ABCMeta) class AExpectedFiles: renderer = None layer = None + multipart = False def __init__(self, layer): self.layer = layer @@ -393,7 +425,8 @@ class AExpectedFiles: file_prefix = cmds.getAttr(ImagePrefixes[self.renderer]) except KeyError: raise UnsupportedRendererException( - "Unsupported renderer {}".format(self.renderer)) + "Unsupported renderer {}".format(self.renderer) + ) return file_prefix def _get_layer_data(self): @@ -419,7 +452,7 @@ class AExpectedFiles: if not file_prefix: raise RuntimeError("Image prefix not set") - default_ext = cmds.getAttr('defaultRenderGlobals.imfPluginKey') + default_ext = cmds.getAttr("defaultRenderGlobals.imfPluginKey") # ________________________________________________ # __________________/ ______________________________________________/ @@ -440,10 +473,10 @@ class AExpectedFiles: layer_name = self.layer if self.layer.startswith("rs_"): layer_name = self.layer[3:] - start_frame = int(self.get_render_attribute('startFrame')) - end_frame = int(self.get_render_attribute('endFrame')) - frame_step = int(self.get_render_attribute('byFrameStep')) - padding = int(self.get_render_attribute('extensionPadding')) + start_frame = int(self.get_render_attribute("startFrame")) + end_frame = int(self.get_render_attribute("endFrame")) + frame_step = int(self.get_render_attribute("byFrameStep")) + padding = int(self.get_render_attribute("extensionPadding")) scene_data = { "frameStart": start_frame, @@ -456,7 +489,7 @@ class AExpectedFiles: "renderer": renderer, "defaultExt": default_ext, "filePrefix": file_prefix, - "enabledAOVs": enabled_aovs + "enabledAOVs": enabled_aovs, } return scene_data @@ -472,21 +505,24 @@ class AExpectedFiles: # in Redshift (R_REMOVE_AOV_TOKEN, ""), (R_CLEAN_FRAME_TOKEN, ""), - (R_CLEAN_EXT_TOKEN, "") + (R_CLEAN_EXT_TOKEN, ""), ) for regex, value in mappings: file_prefix = re.sub(regex, value, file_prefix) for frame in range( - int(layer_data["frameStart"]), - int(layer_data["frameEnd"]) + 1, - int(layer_data["frameStep"])): + int(layer_data["frameStart"]), + int(layer_data["frameEnd"]) + 1, + int(layer_data["frameStep"]), + ): expected_files.append( - '{}.{}.{}'.format(file_prefix, - str(frame).rjust( - layer_data["padding"], "0"), - layer_data["defaultExt"])) + "{}.{}.{}".format( + file_prefix, + str(frame).rjust(layer_data["padding"], "0"), + layer_data["defaultExt"], + ) + ) return expected_files def _generate_aov_file_sequences(self, layer_data): @@ -502,7 +538,7 @@ class AExpectedFiles: (R_SUBSTITUTE_CAMERA_TOKEN, cam), (R_SUBSTITUTE_AOV_TOKEN, aov[0]), (R_CLEAN_FRAME_TOKEN, ""), - (R_CLEAN_EXT_TOKEN, "") + (R_CLEAN_EXT_TOKEN, ""), ) for regex, value in mappings: @@ -510,14 +546,17 @@ class AExpectedFiles: aov_files = [] for frame in range( - int(layer_data["frameStart"]), - int(layer_data["frameEnd"]) + 1, - int(layer_data["frameStep"])): + int(layer_data["frameStart"]), + int(layer_data["frameEnd"]) + 1, + int(layer_data["frameStep"]), + ): aov_files.append( - '{}.{}.{}'.format( + "{}.{}.{}".format( file_prefix, str(frame).rjust(layer_data["padding"], "0"), - aov[1])) + aov[1], + ) + ) # if we have more then one renderable camera, append # camera name to AOV to allow per camera AOVs. @@ -551,17 +590,19 @@ class AExpectedFiles: return expected_files def get_renderable_cameras(self): - cam_parents = [cmds.listRelatives(x, ap=True)[-1] - for x in cmds.ls(cameras=True)] + cam_parents = [ + cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True) + ] renderable_cameras = [] for cam in cam_parents: renderable = False - if self.maya_is_true(cmds.getAttr('{}.renderable'.format(cam))): + if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))): renderable = True for override in self.get_layer_overrides( - '{}.renderable'.format(cam), self.layer): + "{}.renderable".format(cam), self.layer + ): renderable = self.maya_is_true(override) if renderable: @@ -587,16 +628,18 @@ class AExpectedFiles: if connections: for connection in connections: if connection: - node_name = connection.split('.')[0] - if cmds.nodeType(node_name) == 'renderLayer': - attr_name = '%s.value' % '.'.join( - connection.split('.')[:-1]) + node_name = connection.split(".")[0] + if cmds.nodeType(node_name) == "renderLayer": + attr_name = "%s.value" % ".".join( + connection.split(".")[:-1] + ) if node_name == layer: yield cmds.getAttr(attr_name) def get_render_attribute(self, attr): - return lib.get_attr_in_layer("defaultRenderGlobals.{}".format(attr), - layer=self.layer) + return lib.get_attr_in_layer( + "defaultRenderGlobals.{}".format(attr), layer=self.layer + ) class ExpectedFilesArnold(AExpectedFiles): @@ -604,25 +647,28 @@ class ExpectedFilesArnold(AExpectedFiles): # Arnold AOV driver extension mapping # Is there a better way? aiDriverExtension = { - 'jpeg': 'jpg', - 'exr': 'exr', - 'deepexr': 'exr', - 'png': 'png', - 'tiff': 'tif', - 'mtoa_shaders': 'ass', # TODO: research what those last two should be - 'maya': '' + "jpeg": "jpg", + "exr": "exr", + "deepexr": "exr", + "png": "png", + "tiff": "tif", + "mtoa_shaders": "ass", # TODO: research what those last two should be + "maya": "", } def __init__(self, layer): super(ExpectedFilesArnold, self).__init__(layer) - self.renderer = 'arnold' + self.renderer = "arnold" def get_aovs(self): enabled_aovs = [] try: - if not (cmds.getAttr('defaultArnoldRenderOptions.aovMode') - and not cmds.getAttr('defaultArnoldDriver.mergeAOVs')): + if not ( + cmds.getAttr("defaultArnoldRenderOptions.aovMode") + and not cmds.getAttr("defaultArnoldDriver.mergeAOVs") + ): # AOVs are merged in mutli-channel file + self.multipart = True return enabled_aovs except ValueError: # this occurs when Render Setting windows was not opened yet. In @@ -635,46 +681,35 @@ class ExpectedFilesArnold(AExpectedFiles): # AOVs are set to be rendered separately. We should expect # token in path. - ai_aovs = [n for n in cmds.ls(type='aiAOV')] + ai_aovs = [n for n in cmds.ls(type="aiAOV")] for aov in ai_aovs: - enabled = self.maya_is_true( - cmds.getAttr('{}.enabled'.format(aov))) - ai_driver = cmds.listConnections( - '{}.outputs'.format(aov))[0] - ai_translator = cmds.getAttr( - '{}.aiTranslator'.format(ai_driver)) + enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) + ai_driver = cmds.listConnections("{}.outputs".format(aov))[0] + ai_translator = cmds.getAttr("{}.aiTranslator".format(ai_driver)) try: aov_ext = self.aiDriverExtension[ai_translator] except KeyError: - msg = ('Unrecognized arnold ' - 'driver format for AOV - {}').format( - cmds.getAttr('{}.name'.format(aov)) - ) + msg = ( + "Unrecognized arnold " "driver format for AOV - {}" + ).format(cmds.getAttr("{}.name".format(aov))) raise AOVError(msg) for override in self.get_layer_overrides( - '{}.enabled'.format(aov), self.layer): + "{}.enabled".format(aov), self.layer + ): enabled = self.maya_is_true(override) if enabled: # If aov RGBA is selected, arnold will translate it to `beauty` - aov_name = cmds.getAttr('%s.name' % aov) - if aov_name == 'RGBA': - aov_name = 'beauty' - enabled_aovs.append( - ( - aov_name, - aov_ext - ) - ) + aov_name = cmds.getAttr("%s.name" % aov) + if aov_name == "RGBA": + aov_name = "beauty" + enabled_aovs.append((aov_name, aov_ext)) # Append 'beauty' as this is arnolds # default. If token is specified and no AOVs are # defined, this will be used. enabled_aovs.append( - ( - u'beauty', - cmds.getAttr('defaultRenderGlobals.imfPluginKey') - ) + (u"beauty", cmds.getAttr("defaultRenderGlobals.imfPluginKey")) ) return enabled_aovs @@ -688,7 +723,7 @@ class ExpectedFilesVray(AExpectedFiles): def __init__(self, layer): super(ExpectedFilesVray, self).__init__(layer) - self.renderer = 'vray' + self.renderer = "vray" def get_renderer_prefix(self): prefix = super(ExpectedFilesVray, self).get_renderer_prefix() @@ -703,7 +738,9 @@ class ExpectedFilesVray(AExpectedFiles): layer_data = self._get_layer_data() if layer_data.get("enabledAOVs"): - expected_files[0][u"beauty"] = self._generate_single_file_sequence(layer_data) # noqa: E501 + expected_files[0][u"beauty"] = self._generate_single_file_sequence( + layer_data + ) # noqa: E501 return expected_files @@ -712,9 +749,12 @@ class ExpectedFilesVray(AExpectedFiles): try: # really? do we set it in vray just by selecting multichannel exr? - if cmds.getAttr( - "vraySettings.imageFormatStr") == "exr (multichannel)": + if ( + cmds.getAttr("vraySettings.imageFormatStr") + == "exr (multichannel)" + ): # AOVs are merged in mutli-channel file + self.multipart = True return enabled_aovs except ValueError: # this occurs when Render Setting windows was not opened yet. In @@ -724,35 +764,40 @@ class ExpectedFilesVray(AExpectedFiles): # anyway. return enabled_aovs - default_ext = cmds.getAttr('vraySettings.imageFormatStr') + default_ext = cmds.getAttr("vraySettings.imageFormatStr") if default_ext == "exr (multichannel)" or default_ext == "exr (deep)": default_ext = "exr" - vr_aovs = [n for n in cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"])] + vr_aovs = [ + n + for n in cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"] + ) + ] # todo: find out how to detect multichannel exr for vray for aov in vr_aovs: - enabled = self.maya_is_true( - cmds.getAttr('{}.enabled'.format(aov))) + enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) for override in self.get_layer_overrides( - '{}.enabled'.format(aov), 'rs_{}'.format(self.layer)): + "{}.enabled".format(aov), "rs_{}".format(self.layer) + ): enabled = self.maya_is_true(override) if enabled: # todo: find how vray set format for AOVs enabled_aovs.append( - ( - self._get_vray_aov_name(aov), - default_ext) - ) + (self._get_vray_aov_name(aov), default_ext) + ) return enabled_aovs def _get_vray_aov_name(self, node): # Get render element pass type - vray_node_attr = next(attr for attr in cmds.listAttr(node) - if attr.startswith("vray_name")) + vray_node_attr = next( + attr + for attr in cmds.listAttr(node) + if attr.startswith("vray_name") + ) pass_type = vray_node_attr.rsplit("_", 1)[-1] # Support V-Ray extratex explicit name (if set by user) @@ -770,11 +815,11 @@ class ExpectedFilesVray(AExpectedFiles): class ExpectedFilesRedshift(AExpectedFiles): # mapping redshift extension dropdown values to strings - ext_mapping = ['iff', 'exr', 'tif', 'png', 'tga', 'jpg'] + ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] def __init__(self, layer): super(ExpectedFilesRedshift, self).__init__(layer) - self.renderer = 'redshift' + self.renderer = "redshift" def get_renderer_prefix(self): prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix() @@ -789,7 +834,9 @@ class ExpectedFilesRedshift(AExpectedFiles): layer_data = self._get_layer_data() if layer_data.get("enabledAOVs"): - expected_files[0][u"beauty"] = self._generate_single_file_sequence(layer_data) # noqa: E501 + expected_files[0][u"beauty"] = self._generate_single_file_sequence( + layer_data + ) # noqa: E501 return expected_files @@ -798,8 +845,10 @@ class ExpectedFilesRedshift(AExpectedFiles): try: if self.maya_is_true( - cmds.getAttr("redshiftOptions.exrForceMultilayer")): + cmds.getAttr("redshiftOptions.exrForceMultilayer") + ): # AOVs are merged in mutli-channel file + self.multipart = True return enabled_aovs except ValueError: # this occurs when Render Setting windows was not opened yet. In @@ -810,34 +859,30 @@ class ExpectedFilesRedshift(AExpectedFiles): return enabled_aovs default_ext = self.ext_mapping[ - cmds.getAttr('redshiftOptions.imageFormat') + cmds.getAttr("redshiftOptions.imageFormat") ] - rs_aovs = [n for n in cmds.ls(type='RedshiftAOV')] + rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")] # todo: find out how to detect multichannel exr for redshift for aov in rs_aovs: - enabled = self.maya_is_true( - cmds.getAttr('{}.enabled'.format(aov))) + enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) for override in self.get_layer_overrides( - '{}.enabled'.format(aov), self.layer): + "{}.enabled".format(aov), self.layer + ): enabled = self.maya_is_true(override) if enabled: enabled_aovs.append( - ( - cmds.getAttr('%s.name' % aov), - default_ext - ) + (cmds.getAttr("%s.name" % aov), default_ext) ) return enabled_aovs class ExpectedFilesRenderman(AExpectedFiles): - def __init__(self, layer): super(ExpectedFilesRenderman, self).__init__(layer) - self.renderer = 'renderman' + self.renderer = "renderman" def get_aovs(self): enabled_aovs = [] @@ -849,19 +894,14 @@ class ExpectedFilesRenderman(AExpectedFiles): if aov_name == "rmanDefaultDisplay": aov_name = "beauty" - enabled = self.maya_is_true( - cmds.getAttr("{}.enable".format(aov))) + enabled = self.maya_is_true(cmds.getAttr("{}.enable".format(aov))) for override in self.get_layer_overrides( - '{}.enable'.format(aov), self.layer): + "{}.enable".format(aov), self.layer + ): enabled = self.maya_is_true(override) if enabled: - enabled_aovs.append( - ( - aov_name, - default_ext - ) - ) + enabled_aovs.append((aov_name, default_ext)) return enabled_aovs @@ -881,9 +921,9 @@ class ExpectedFilesRenderman(AExpectedFiles): for aov, files in expected_files[0].items(): new_files = [] for file in files: - new_file = "{}/{}/{}".format(layer_data["sceneName"], - layer_data["layerName"], - file) + new_file = "{}/{}/{}".format( + layer_data["sceneName"], layer_data["layerName"], file + ) new_files.append(new_file) new_aovs[aov] = new_files @@ -891,9 +931,8 @@ class ExpectedFilesRenderman(AExpectedFiles): class ExpectedFilesMentalray(AExpectedFiles): - def __init__(self, layer): - raise UnimplementedRendererException('Mentalray not implemented') + raise UnimplementedRendererException("Mentalray not implemented") def get_aovs(self): return [] From 08a861d2a51eb26722971596ec508e956d9e81de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 30 Mar 2020 15:09:37 +0200 Subject: [PATCH 187/327] move multipart flag to tags on representation --- pype/plugins/global/publish/extract_burnin.py | 2 +- pype/plugins/global/publish/extract_jpeg.py | 2 +- pype/plugins/global/publish/extract_review.py | 7 ++++--- pype/plugins/global/publish/submit_publish_job.py | 4 +++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 9ef97e19d0..a36ba802df 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -65,7 +65,7 @@ class ExtractBurnin(pype.api.Extractor): for i, repre in enumerate(instance.data["representations"]): self.log.debug("__ i: `{}`, repre: `{}`".format(i, repre)) - if repre.get("multipartExr", False) is True: + if "multipartExr" in repre.get("tags", []): # ffmpeg doesn't support multipart exrs continue diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index c856007d31..40e086db99 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -36,7 +36,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if not isinstance(repre['files'], list): continue - if repre.get("multipartExr", False) is True: + if "multipartExr" in tags: # ffmpeg doesn't support multipart exrs continue diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 02504edd14..625c96566d 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -57,15 +57,16 @@ class ExtractReview(pyblish.api.InstancePlugin): # filter out mov and img sequences representations_new = representations[:] for repre in representations: - if repre.get("multipartExr", False) is True: - # ffmpeg doesn't support multipart exrs - continue if repre['ext'] not in self.ext_filter: continue tags = repre.get("tags", []) + if "multipartExr" in tags: + # ffmpeg doesn't support multipart exrs + continue + if "thumbnail" in tags: continue diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 6c88b5d2aa..bc9d9ba1ae 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -443,10 +443,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "stagingDir": os.path.dirname(list(c)[0]), "anatomy_template": "render", "fps": instance.get("fps"), - "multipartExr": instance.get("multipartExr"), "tags": ["review", "preview"] if preview else [], } + if instance.get("multipartExr", False): + rep["tags"].append["multipartExr"] + representations.append(rep) self._solve_families(instance, preview) From d9ea84777c4484e2bcb774f904b173ae08f3e532 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Mar 2020 15:50:36 +0200 Subject: [PATCH 188/327] environment AVALON_PROJECT is sent to publish job an metadata json stars with {root} --- pype/plugins/global/publish/collect_rendered_files.py | 3 +++ pype/plugins/global/publish/submit_publish_job.py | 3 ++- pype/scripts/publish_filesequence.py | 6 ------ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index 552fd49f6d..7ae68f8e44 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -84,6 +84,9 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): session_set = False for path in paths: + path = path.format(**{ + "root": os.environ["PYPE_STUDIO_PROJECTS_PATH"] + }) data = self._load_json(path) if not session_set: self.log.info("Setting session using data from file") diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index dcf19ae32c..c7124d73a5 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -195,7 +195,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): mount_root = os.path.normpath(os.environ["PYPE_STUDIO_PROJECTS_MOUNT"]) network_root = os.environ["PYPE_STUDIO_PROJECTS_PATH"] metadata_path = metadata_path.replace(mount_root, network_root) - metadata_path = os.path.normpath(metadata_path) + metadata_path = metadata_path.replace(network_root, "{root}") # Generate the payload for Deadline submission payload = { @@ -225,6 +225,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): environment = job["Props"].get("Env", {}) environment["PYPE_METADATA_FILE"] = metadata_path + environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] i = 0 for index, key in enumerate(environment): if key.upper() in self.enviro_filter: diff --git a/pype/scripts/publish_filesequence.py b/pype/scripts/publish_filesequence.py index fe795564a5..168011391a 100644 --- a/pype/scripts/publish_filesequence.py +++ b/pype/scripts/publish_filesequence.py @@ -90,12 +90,6 @@ def __main__(): paths = kwargs.paths or [os.environ.get("PYPE_METADATA_FILE")] or [os.getcwd()] # noqa - for path in paths: - data = _load_json(path) - log.info("Setting session using data from file") - os.environ["AVALON_PROJECT"] = data["session"]["AVALON_PROJECT"] - break - args = [ os.path.join(pype_root, pype_command), "publish", From 66f7367524dbb3d749cf6564fbd48fbe00b74a34 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 30 Mar 2020 17:35:16 +0200 Subject: [PATCH 189/327] minor refactor --- ...ract_quicktime.py => extract_playblast.py} | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) rename pype/plugins/maya/publish/{extract_quicktime.py => extract_playblast.py} (87%) diff --git a/pype/plugins/maya/publish/extract_quicktime.py b/pype/plugins/maya/publish/extract_playblast.py similarity index 87% rename from pype/plugins/maya/publish/extract_quicktime.py rename to pype/plugins/maya/publish/extract_playblast.py index 29d6b78051..cc307d6afc 100644 --- a/pype/plugins/maya/publish/extract_quicktime.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -3,24 +3,23 @@ import glob import contextlib import clique import capture -# + import pype.maya.lib as lib import pype.api -# -from maya import cmds, mel + +from maya import cmds import pymel.core as pm -# TODO: move codec settings to presets -class ExtractQuicktime(pype.api.Extractor): - """Extract Quicktime from viewport capture. +class ExtractPlayblast(pype.api.Extractor): + """Extract viewport playblast. Takes review camera and creates review Quicktime video based on viewport capture. """ - label = "Quicktime" + label = "Extract Playblast" hosts = ["maya"] families = ["review"] optional = True @@ -29,7 +28,7 @@ class ExtractQuicktime(pype.api.Extractor): self.log.info("Extracting capture..") # get scene fps - fps = mel.eval('currentTimeUnitToFPS()') + fps = instance.data.get("fps") or instance.context.data.get("fps") # if start and end frames cannot be determined, get them # from Maya timeline @@ -39,6 +38,7 @@ class ExtractQuicktime(pype.api.Extractor): start = cmds.playbackOptions(query=True, animationStartTime=True) if end is None: end = cmds.playbackOptions(query=True, animationEndTime=True) + self.log.info("start: {}, end: {}".format(start, end)) # get cameras @@ -47,7 +47,7 @@ class ExtractQuicktime(pype.api.Extractor): try: preset = lib.load_capture_preset(data=capture_preset) - except: + except Exception: preset = {} self.log.info('using viewport preset: {}'.format(preset)) @@ -55,19 +55,10 @@ class ExtractQuicktime(pype.api.Extractor): preset['format'] = "image" # preset['compression'] = "qt" preset['quality'] = 95 - preset['compression'] = "jpg" + preset['compression'] = "png" preset['start_frame'] = start preset['end_frame'] = end preset['camera_options'] = { - "displayGateMask": False, - "displayResolution": False, - "displayFilmGate": False, - "displayFieldChart": False, - "displaySafeAction": False, - "displaySafeTitle": False, - "displayFilmPivot": False, - "displayFilmOrigin": False, - "overscan": 1.0, "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), } @@ -90,8 +81,8 @@ class ExtractQuicktime(pype.api.Extractor): filename = preset.get("filename", "%TEMP%") # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between playblast - # and viewer + # viewer opening call to allow a signal to trigger between + # playblast and viewer preset['viewer'] = False # Remove panel key since it's internal value to capture_gui @@ -112,8 +103,8 @@ class ExtractQuicktime(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'mov', - 'ext': 'mov', + 'name': 'png', + 'ext': 'png', 'files': collected_frames, "stagingDir": stagingdir, "frameStart": start, @@ -133,7 +124,6 @@ class ExtractQuicktime(pype.api.Extractor): To workaround this we just glob.glob() for any file extensions and assume the latest modified file is the correct file and return it. - """ # Catch cancelled playblast if filepath is None: @@ -164,7 +154,6 @@ class ExtractQuicktime(pype.api.Extractor): return filepath - @contextlib.contextmanager def maintained_time(): ct = cmds.currentTime(query=True) From 3c70a91c3357ae95a169783474568099fe836f17 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 30 Mar 2020 23:25:55 +0200 Subject: [PATCH 190/327] bump version --- pype/__init__.py | 1 - pype/version.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 pype/version.py diff --git a/pype/__init__.py b/pype/__init__.py index 5cd9832558..803d3fa60e 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -9,7 +9,6 @@ from pypeapp import config import logging log = logging.getLogger(__name__) -__version__ = "2.6.0" PROJECT_PLUGINS_PATH = os.environ.get("PYPE_PROJECT_PLUGINS") PACKAGE_DIR = os.path.dirname(__file__) diff --git a/pype/version.py b/pype/version.py new file mode 100644 index 0000000000..2614ce9d96 --- /dev/null +++ b/pype/version.py @@ -0,0 +1 @@ +__version__ = "2.7.0" From 3a3bb0a90c8505cbaafb92532ed9b764454a9cf7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 31 Mar 2020 17:58:48 +0200 Subject: [PATCH 191/327] feat(premiere): conversion to pype2 - wip --- pype/avalon_apps/rest_api.py | 17 ++++ pype/hooks/premiere/prelaunch.py | 57 +++++++++++++ pype/premiere/__init__.py | 39 +++++---- pype/premiere/lib.py | 137 +++++++++++++++++++++++++++++++ pype/premiere/templates.py | 41 --------- 5 files changed, 234 insertions(+), 57 deletions(-) create mode 100644 pype/hooks/premiere/prelaunch.py create mode 100644 pype/premiere/lib.py delete mode 100644 pype/premiere/templates.py diff --git a/pype/avalon_apps/rest_api.py b/pype/avalon_apps/rest_api.py index ae027383a1..af40bfe920 100644 --- a/pype/avalon_apps/rest_api.py +++ b/pype/avalon_apps/rest_api.py @@ -68,6 +68,23 @@ class AvalonRestApi(RestApi): _asset, identificator, project_name )) + @RestApi.route("/publish/", url_prefix="/premiere", methods="GET") + def publish(self, request): + """ + http://localhost:8021/premiere/publish/shot021?json_in=this/path/file_in.json&json_out=this/path/file_out.json + """ + asset_name = request.url_data["asset_name"] + query = request.query + data = request.request_data + + output = { + "message": "Got your data. Thanks.", + "your_data": data, + "your_query": query, + "your_asset_is": asset_name + } + return CallbackResult(data=self.result_to_json(output)) + def result_to_json(self, result): """ Converts result of MongoDB query to dict without $oid (ObjectId) keys with help of regex matching. diff --git a/pype/hooks/premiere/prelaunch.py b/pype/hooks/premiere/prelaunch.py new file mode 100644 index 0000000000..845ee63fb7 --- /dev/null +++ b/pype/hooks/premiere/prelaunch.py @@ -0,0 +1,57 @@ +import logging +import os + +from pype.lib import PypeHook +from pypeapp import Logger + +log = logging.getLogger(__name__) + + +class PremierePrelaunch(PypeHook): + """ + This hook will check if current workfile path has Unreal + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Unreal launcher + shell script. + """ + + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + if not env: + env = os.environ + asset = env["AVALON_ASSET"] + task = env["AVALON_TASK"] + workdir = env["AVALON_WORKDIR"] + project_name = f"{asset}_{task}" + + import avalon.api + import pype.premiere + avalon.api.install(pype.premiere) + + try: + __import__("pype.premiere") + __import__("pyblish") + + except ImportError as e: + print traceback.format_exc() + print("pyblish: Could not load integration: %s " % e) + + else: + # Setup integration + import pype.premiere.lib + pype.premiere.lib.setup() + + self.log.debug("_ self.signature: `{}`".format(self.signature)) + self.log.debug("_ asset: `{}`".format(asset)) + self.log.debug("_ task: `{}`".format(task)) + self.log.debug("_ workdir: `{}`".format(workdir)) + self.log.debug("_ project_name: `{}`".format(project_name)) + + return True diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 287f07f433..49912ef309 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -5,15 +5,24 @@ import shutil from pysync import walktree from avalon import api as avalon -from avalon.lib import launch from pyblish import api as pyblish from app import api as app -from pprint import pprint from .. import api - - import requests +from .pipeline import ( + install, + uninstall, + reload_pipeline, + ls +) + +__all__ = [ + "install", + "uninstall", + "reload_pipeline", + "ls" +] log = api.Logger.getLogger(__name__, "premiere") @@ -43,6 +52,8 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "premiere", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "premiere", "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "premiere", "inventory") +log.debug("_clearing_cache: {}".format(_clearing_cache)) + def clearing_caches_ui(): '''Before every start of premiere it will make sure there is not outdated stuff in cep_cache dir''' @@ -104,23 +115,19 @@ def extensions_sync(): def install(): - api.set_avalon_workdir() + log.info("Registering Premiera plug-ins..") reg_paths = request_aport("/api/register_plugin_path", {"publish_path": PUBLISH_PATH}) - # avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - # avalon.register_plugin_path(avalon.Creator, CREATE_PATH) - # avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) - # Disable all families except for the ones we explicitly want to see - # family_states = [ - # "imagesequence", - # "mov" - # - # ] - # avalon.data["familiesStateDefault"] = False - # avalon.data["familiesStateToggled"] = family_states + family_states = [ + "imagesequence", + "mov" + + ] + avalon.data["familiesStateDefault"] = False + avalon.data["familiesStateToggled"] = family_states # load data from templates api.load_data_from_templates() diff --git a/pype/premiere/lib.py b/pype/premiere/lib.py new file mode 100644 index 0000000000..154c55bb04 --- /dev/null +++ b/pype/premiere/lib.py @@ -0,0 +1,137 @@ +import os +import importlib +from pyblish import api as pyblish +from avalon import api +import logging + + +log = logging.getLogger(__name__) + +AVALON_CONFIG = os.environ["AVALON_CONFIG"] + + +def ls(): + pass + + +def reload_pipeline(): + """Attempt to reload pipeline at run-time. + + CAUTION: This is primarily for development and debugging purposes. + + """ + + import importlib + + api.uninstall() + + for module in ("avalon.io", + "avalon.lib", + "avalon.pipeline", + "avalon.api", + "avalon.tools", + + "avalon.tools.loader.app", + "avalon.tools.creator.app", + "avalon.tools.manager.app", + + "avalon.premiere", + "avalon.premiere.pipeline", + "{}".format(AVALON_CONFIG) + ): + log.info("Reloading module: {}...".format(module)) + module = importlib.import_module(module) + reload(module) + + import avalon.premiere + api.install(avalon.premiere) + + +def install(config): + """Install Premiere-specific functionality of avalon-core. + + This is where you install menus and register families, data + and loaders into Premiere. + + It is called automatically when installing via `api.install(premiere)`. + + See the Maya equivalent for inspiration on how to implement this. + + """ + + pyblish.register_host("premiere") + # Trigger install on the config's "premiere" package + config = find_host_config(config) + + if hasattr(config, "install"): + config.install() + + log.info("config.premiere installed") + + +def find_host_config(config): + try: + config = importlib.import_module(config.__name__ + ".premiere") + except ImportError as exc: + if str(exc) != "No module name {}".format( + config.__name__ + ".premiere"): + raise + config = None + + return config + + +def uninstall(config): + """Uninstall all tha was installed + + This is where you undo everything that was done in `install()`. + That means, removing menus, deregistering families and data + and everything. It should be as though `install()` was never run, + because odds are calling this function means the user is interested + in re-installing shortly afterwards. If, for example, he has been + modifying the menu or registered families. + + """ + config = find_host_config(config) + if hasattr(config, "uninstall"): + config.uninstall() + + pyblish.deregister_host("premiere") + + +def get_anatomy(**kwarg): + return pype.Anatomy + + +def get_dataflow(**kwarg): + log.info(kwarg) + host = kwarg.get("host", "premiere") + cls = kwarg.get("class", None) + preset = kwarg.get("preset", None) + assert any([host, cls]), log.error("premiera.lib.get_dataflow():" + "Missing mandatory kwargs `host`, `cls`") + + pr_dataflow = getattr(pype.Dataflow, str(host), None) + pr_dataflow_node = getattr(pr_dataflow.nodes, str(cls), None) + if preset: + pr_dataflow_node = getattr(pr_dataflow_node, str(preset), None) + + log.info("Dataflow: {}".format(pr_dataflow_node)) + return pr_dataflow_node + + +def get_colorspace(**kwarg): + log.info(kwarg) + host = kwarg.get("host", "premiere") + cls = kwarg.get("class", None) + preset = kwarg.get("preset", None) + assert any([host, cls]), log.error("premiera.templates.get_colorspace():" + "Missing mandatory kwargs `host`, `cls`") + + pr_colorspace = getattr(pype.Colorspace, str(host), None) + pr_colorspace_node = getattr(pr_colorspace, str(cls), None) + if preset: + pr_colorspace_node = getattr(pr_colorspace_node, str(preset), None) + + log.info("Colorspace: {}".format(pr_colorspace_node)) + return pr_colorspace_node diff --git a/pype/premiere/templates.py b/pype/premiere/templates.py deleted file mode 100644 index 33a7a6ff61..0000000000 --- a/pype/premiere/templates.py +++ /dev/null @@ -1,41 +0,0 @@ -from pype import api as pype - -log = pype.Logger.getLogger(__name__, "premiere") - - -def get_anatomy(**kwarg): - return pype.Anatomy - - -def get_dataflow(**kwarg): - log.info(kwarg) - host = kwarg.get("host", "premiere") - cls = kwarg.get("class", None) - preset = kwarg.get("preset", None) - assert any([host, cls]), log.error("premiera.templates.get_dataflow():" - "Missing mandatory kwargs `host`, `cls`") - - pr_dataflow = getattr(pype.Dataflow, str(host), None) - pr_dataflow_node = getattr(pr_dataflow.nodes, str(cls), None) - if preset: - pr_dataflow_node = getattr(pr_dataflow_node, str(preset), None) - - log.info("Dataflow: {}".format(pr_dataflow_node)) - return pr_dataflow_node - - -def get_colorspace(**kwarg): - log.info(kwarg) - host = kwarg.get("host", "premiere") - cls = kwarg.get("class", None) - preset = kwarg.get("preset", None) - assert any([host, cls]), log.error("premiera.templates.get_colorspace():" - "Missing mandatory kwargs `host`, `cls`") - - pr_colorspace = getattr(pype.Colorspace, str(host), None) - pr_colorspace_node = getattr(pr_colorspace, str(cls), None) - if preset: - pr_colorspace_node = getattr(pr_colorspace_node, str(preset), None) - - log.info("Colorspace: {}".format(pr_colorspace_node)) - return pr_colorspace_node From 0ce4a3d8c57cea540709149271f3d1162e4b3082 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Apr 2020 15:14:14 +0200 Subject: [PATCH 192/327] feat(premiere): adding pysync.py to vendors --- pype/vendor/pysync.py | 440 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 pype/vendor/pysync.py diff --git a/pype/vendor/pysync.py b/pype/vendor/pysync.py new file mode 100644 index 0000000000..5c42b63482 --- /dev/null +++ b/pype/vendor/pysync.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python +""" +A Python implementation of rsync + +This is a demonstration implementation of the rsync algorithm in Python. It is +not fast and is not optimised. The primary aim is to provide a simple example +implementation of the algorithm for reference, so code clarity is more important +than performance. Ideas have been liberaly taken from libhsync, xdelta and +rsync. + + $Id: pysync.py 1.21 Sat, 18 Oct 2003 00:17:54 +1000 abo $ +Author : Donovan Baarda +License : LGPL +Download: ftp://minkirri.apana.org.au/pub/python + +Requires: sys, zlib, types, md4sum + rollsum (included md4sum-alike rollsum wrapper) + +Usage: + # Low level API signature calculation + sig=calcsig(oldfile) + + # Low level API rsync style incremental delta calc from sig and newdata + delta=rdeltaobj(sig) + # or for xdelta style incremental delta calc from oldfile and newdata + # delta=xdeltaobj(oldfile) + incdelta=delta.calcdelta(newdata) + : + incdelta=delta.flush() + + # Low level API applying incremental delta to oldfile to get newdata + patch=patchobj(oldfile) + newdata=patch.calcpatch(incdelta) + : + + # High level API + sig=calcsig(oldfile) # create a sig object + delta=calcrdelta(sig,newfile) # create a rdelta object + delta=calcxdelta(oldfile,newfile) # create a xdelta object + calcpatch(oldfile,delta,newfile) # apply a delta object + + # File level API + stats=filesig(oldfile,sigfile) # create sigfile + stats=filerdelta(sigfile,newfile,diffile) # create a rdelta diffile + stats=filexdelta(oldfile,newfile,diffile) # create a xdelta diffile + stats=filepatch(oldfile,diffile,newfile) # apply a diffile + +Where: + sig - a signature object + delta - a delta object + stats - a statistics object that can be printed + newdata - the target incremental data sequence + incdelta - the incremental delta list + oldfile - the source file + newfile - the target file + sigfile - the signature file + diffile - the delta file + +a delta is implemented as a list containing a sequence of (context) +compressed insert strings and (offset,length) match pairs. + +A signature is a (length, blocksize, sigtable) tuple, where length and blocksize +are integers. The sigtable is implemented as a rollsum keyed dictionary of +md4sum keyed dictionaries containing offsets. +ie sigtable[rollsum][md4sum]=offset + +Note rsync uses md4sums because they are faster than md5sums, but +python doesn't have a built in md4sum wrapper. I use an md4 module +based on the libmd RSA md4 implementation and a modified md5module.c + +thoughts on using zlib to compress deltas; + +1) compress the whole instruction stream +2) compress the inserts only using Z_SYNC_FLUSH to delimit and put + inserts into the instruction stream. +3) compress everything using Z_SYNC_FLUSH to delimit boundaries, inserting + only output for inserts into the instruction stream (rsync?) +4) compress the insert stream without Z_SYNC_FLUSH and put offset/lengths in + instruction stream, sending compressed inserts seperately (xdelta?) + +it depends on how zlib performs with heaps of Z_SYNC_FLUSH's. If it hurts +performance badly, then 4 is best. Otherwise, it would pay to see if zlib +improves compression with inserted context data not included in the output +stream. + +My tests on zlib suggest that syncs do hurt a little, but dispite that +including context by compressing _all_ the data, not just the deltas, gives +the best compression. Unfortunately this has extra load on applying patches +because it requires all data to be compressed to supply the compression +stream for the missing context info for decompression. + +thoughts on instruction stream; + +use fixed length and put only offsets into instruction stream for matches, +put inserts directly into the instruction stream. + +use source/offset/length in the instruction stream, and make the inserts a +seperate source (xdelta). + +by putting offset/length in the instruction stream rather than just block id's +the instruction stream becomes more generic... anything that can generate +offset/lengths can generate patches... possibly more optimal ones than rsync +(ie, xdelta's largest possible match type tricks). + +Including a source along with offset/length means multiple sources can be used +for a single patch (like xdelta), though this can be fudged by appending sources +into one long stream. + +""" +# psyco is a python accelerator which speeds up pysync by 33% +try: + import psyco + psyco.profile() +except: + pass +from zlib import * +from types import TupleType,StringType +import md4,rollsum + +# the default block size used throughout. This is carefuly chosen to try and +# avoid the zlib decompressor sync bug which strikes at about 16K +BLOCK_SIZE=8192 + +# the various supported flush modes. +R_SYNC_FLUSH=Z_SYNC_FLUSH +R_FINISH=Z_FINISH + +def calcsig(oldfile,blocksize=BLOCK_SIZE): + "Calculates and returns a signature" + offset=0 + sigtable={} + data=oldfile.read(blocksize) + while data: + sum=md4.new(data).digest() + sig=rollsum.new(data).digest() + try: + sigtable[sig][sum]=offset + except KeyError: + sigtable[sig]={} + sigtable[sig][sum]=offset + offset=offset+len(data) + data=oldfile.read(blocksize) + return (offset,blocksize,sigtable) + +class rdeltaobj: + "Incremental delta calculation class for deltas from signature to newfile" + def __init__(self,(length,blocksize,sigtable)): + self.length = length + self.blocksize = blocksize + self.sigtable = sigtable + self.data = "" # the yet to be processed data + self.pos = 0 # the position processed up to in data + self.sig = None # the rollsum sig of the next data block + self.last = None # the last processed delta match/miss + self.delta = [] # the delta list calculated thus far + self.comp = compressobj(9) # the delta zlib compressor object + def _compress(self): + "compress and return up to pos, adjusting data and pos" + data=buffer(self.data,0,self.pos) + self.data,self.pos=buffer(self.data,self.pos),0 + return self.comp.compress(data) + def _flush(self,mode=R_SYNC_FLUSH): + "compress, flush, and return up to pos, adjusting data and pos" + return self._compress()+self.comp.flush(mode) + def _findmatch(self): + "return a match tuple, or raise KeyError if there isn't one" + # get the rollsum digest, calculating sig if needed + try: + sig=self.sig.digest() + except AttributeError: + self.sig=rollsum.new(buffer(self.data,self.pos,self.blocksize)) + sig=self.sig.digest() + # get the matching offset, if it exists, otherwise raise KeyError + sumtable=self.sigtable[sig] + sum=md4.new(buffer(self.data,self.pos,self.blocksize)) + return sumtable[sum.digest()],self.sig.count + def _appendmatch(self,(offset,length)): + "append a match to delta" + # if last was a match that can be extended, extend it + if type(self.last)==TupleType and self.last[0]+self.last[1]==offset: + self.last=(self.last[0],self.last[1]+length) + else: + # else appendflush the last value + self._appendflush(R_SYNC_FLUSH) + # make this match the new last + self.last=(offset,length) + # increment pos and compress the matched data for context + self.pos=self.pos+length + self._compress() + def _appendmiss(self,length): + "append a miss to delta" + if type(self.last)!=StringType: + # if last was not a miss, appendflush the last value + self._appendflush(R_SYNC_FLUSH) + # make this miss the new last + self.last="" + # increment pos and compress if greater than blocksize + self.pos=self.pos+length + #if self.pos >= self.blocksize: + # self.last=self.last+self._compress() + def _appendflush(self,mode=R_FINISH): + "append a flush to delta" + if type(self.last)==StringType: + self.delta.append(self.last+self._flush(mode)) + elif self.last: + self.delta.append(self.last) + self._flush(mode) + self.last=None + def calcdelta(self,newdata): + "incrementaly calculates and returns a delta list" + self.data=self.data+newdata + while self.pos+self.blocksize=2 and argv[1]=="signature": + oldfile,sigfile=openarg(2,'rb'),openarg(3,'wb') + stats=filesig(oldfile,sigfile,1024) + stderr.write(str(stats)) + elif len(argv)>=3 and argv[1]=="rdelta": + sigfile,newfile,diffile=openarg(2,'rb'),openarg(3,'rb'),openarg(4,'wb') + stats=filerdelta(sigfile,newfile,diffile) + stderr.write(str(stats)) + elif len(argv)>=3 and argv[1]=="xdelta": + oldfile,newfile,diffile=openarg(2,'rb'),openarg(3,'rb'),openarg(4,'wb') + stats=filexdelta(oldfile,newfile,diffile) + stderr.write(str(stats)) + elif len(argv)>=3 and argv[1]=="patch": + oldfile,diffile,newfile=openarg(2,'rb'),openarg(3,'rb'),openarg(4,'wb') + stats=filepatch(oldfile,diffile,newfile) + stderr.write(str(stats)) + else: + print """ +Usage: + %s signature [ []] + ... generates signature file from + + %s rdelta [ []] + ... generates rdelta file for from + + %s xdelta [ []] + ... generates xdelta file for from + + %s patch [ []] + ... applies delta file to to generate + +Where file parameters ommitted or specified as '-' indicate standard +input or output as appropriate. +""" % ((os.path.basename(argv[0]),) * 4) + exit(1) From 3facd293188b3f3d5c149187a6365b1c4de18880 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Apr 2020 15:17:33 +0200 Subject: [PATCH 193/327] feat(premiere): updating pysync in vendors --- pype/vendor/pysync.py | 632 ++++++++++++++---------------------------- 1 file changed, 204 insertions(+), 428 deletions(-) diff --git a/pype/vendor/pysync.py b/pype/vendor/pysync.py index 5c42b63482..14a6dda34c 100644 --- a/pype/vendor/pysync.py +++ b/pype/vendor/pysync.py @@ -1,440 +1,216 @@ -#!/usr/bin/env python -""" -A Python implementation of rsync +#!/usr/local/bin/python3 +# https://github.com/snullp/pySync/blob/master/pySync.py -This is a demonstration implementation of the rsync algorithm in Python. It is -not fast and is not optimised. The primary aim is to provide a simple example -implementation of the algorithm for reference, so code clarity is more important -than performance. Ideas have been liberaly taken from libhsync, xdelta and -rsync. +import sys +import shutil +import os +import time +import configparser +from os.path import ( + getsize, + getmtime, + isfile, + isdir, + join, + abspath, + expanduser, + realpath +) +import logging - $Id: pysync.py 1.21 Sat, 18 Oct 2003 00:17:54 +1000 abo $ -Author : Donovan Baarda -License : LGPL -Download: ftp://minkirri.apana.org.au/pub/python +log = logging.getLogger(__name__) -Requires: sys, zlib, types, md4sum - rollsum (included md4sum-alike rollsum wrapper) +ignoreFiles = ("Thumbs.db", ".DS_Store") -Usage: - # Low level API signature calculation - sig=calcsig(oldfile) - - # Low level API rsync style incremental delta calc from sig and newdata - delta=rdeltaobj(sig) - # or for xdelta style incremental delta calc from oldfile and newdata - # delta=xdeltaobj(oldfile) - incdelta=delta.calcdelta(newdata) - : - incdelta=delta.flush() +# this feature is not yet implemented +ignorePaths = [] - # Low level API applying incremental delta to oldfile to get newdata - patch=patchobj(oldfile) - newdata=patch.calcpatch(incdelta) - : +if os.name == 'nt': + # msvcrt can't function correctly in IDLE + if 'idlelib.run' in sys.modules: + print("Please don't run this script in IDLE.") + sys.exit(0) + import msvcrt - # High level API - sig=calcsig(oldfile) # create a sig object - delta=calcrdelta(sig,newfile) # create a rdelta object - delta=calcxdelta(oldfile,newfile) # create a xdelta object - calcpatch(oldfile,delta,newfile) # apply a delta object - - # File level API - stats=filesig(oldfile,sigfile) # create sigfile - stats=filerdelta(sigfile,newfile,diffile) # create a rdelta diffile - stats=filexdelta(oldfile,newfile,diffile) # create a xdelta diffile - stats=filepatch(oldfile,diffile,newfile) # apply a diffile - -Where: - sig - a signature object - delta - a delta object - stats - a statistics object that can be printed - newdata - the target incremental data sequence - incdelta - the incremental delta list - oldfile - the source file - newfile - the target file - sigfile - the signature file - diffile - the delta file - -a delta is implemented as a list containing a sequence of (context) -compressed insert strings and (offset,length) match pairs. - -A signature is a (length, blocksize, sigtable) tuple, where length and blocksize -are integers. The sigtable is implemented as a rollsum keyed dictionary of -md4sum keyed dictionaries containing offsets. -ie sigtable[rollsum][md4sum]=offset - -Note rsync uses md4sums because they are faster than md5sums, but -python doesn't have a built in md4sum wrapper. I use an md4 module -based on the libmd RSA md4 implementation and a modified md5module.c - -thoughts on using zlib to compress deltas; - -1) compress the whole instruction stream -2) compress the inserts only using Z_SYNC_FLUSH to delimit and put - inserts into the instruction stream. -3) compress everything using Z_SYNC_FLUSH to delimit boundaries, inserting - only output for inserts into the instruction stream (rsync?) -4) compress the insert stream without Z_SYNC_FLUSH and put offset/lengths in - instruction stream, sending compressed inserts seperately (xdelta?) - -it depends on how zlib performs with heaps of Z_SYNC_FLUSH's. If it hurts -performance badly, then 4 is best. Otherwise, it would pay to see if zlib -improves compression with inserted context data not included in the output -stream. - -My tests on zlib suggest that syncs do hurt a little, but dispite that -including context by compressing _all_ the data, not just the deltas, gives -the best compression. Unfortunately this has extra load on applying patches -because it requires all data to be compressed to supply the compression -stream for the missing context info for decompression. - -thoughts on instruction stream; - -use fixed length and put only offsets into instruction stream for matches, -put inserts directly into the instruction stream. - -use source/offset/length in the instruction stream, and make the inserts a -seperate source (xdelta). - -by putting offset/length in the instruction stream rather than just block id's -the instruction stream becomes more generic... anything that can generate -offset/lengths can generate patches... possibly more optimal ones than rsync -(ie, xdelta's largest possible match type tricks). - -Including a source along with offset/length means multiple sources can be used -for a single patch (like xdelta), though this can be fudged by appending sources -into one long stream. - -""" -# psyco is a python accelerator which speeds up pysync by 33% -try: - import psyco - psyco.profile() -except: - pass -from zlib import * -from types import TupleType,StringType -import md4,rollsum - -# the default block size used throughout. This is carefuly chosen to try and -# avoid the zlib decompressor sync bug which strikes at about 16K -BLOCK_SIZE=8192 - -# the various supported flush modes. -R_SYNC_FLUSH=Z_SYNC_FLUSH -R_FINISH=Z_FINISH - -def calcsig(oldfile,blocksize=BLOCK_SIZE): - "Calculates and returns a signature" - offset=0 - sigtable={} - data=oldfile.read(blocksize) - while data: - sum=md4.new(data).digest() - sig=rollsum.new(data).digest() - try: - sigtable[sig][sum]=offset - except KeyError: - sigtable[sig]={} - sigtable[sig][sum]=offset - offset=offset+len(data) - data=oldfile.read(blocksize) - return (offset,blocksize,sigtable) - -class rdeltaobj: - "Incremental delta calculation class for deltas from signature to newfile" - def __init__(self,(length,blocksize,sigtable)): - self.length = length - self.blocksize = blocksize - self.sigtable = sigtable - self.data = "" # the yet to be processed data - self.pos = 0 # the position processed up to in data - self.sig = None # the rollsum sig of the next data block - self.last = None # the last processed delta match/miss - self.delta = [] # the delta list calculated thus far - self.comp = compressobj(9) # the delta zlib compressor object - def _compress(self): - "compress and return up to pos, adjusting data and pos" - data=buffer(self.data,0,self.pos) - self.data,self.pos=buffer(self.data,self.pos),0 - return self.comp.compress(data) - def _flush(self,mode=R_SYNC_FLUSH): - "compress, flush, and return up to pos, adjusting data and pos" - return self._compress()+self.comp.flush(mode) - def _findmatch(self): - "return a match tuple, or raise KeyError if there isn't one" - # get the rollsum digest, calculating sig if needed - try: - sig=self.sig.digest() - except AttributeError: - self.sig=rollsum.new(buffer(self.data,self.pos,self.blocksize)) - sig=self.sig.digest() - # get the matching offset, if it exists, otherwise raise KeyError - sumtable=self.sigtable[sig] - sum=md4.new(buffer(self.data,self.pos,self.blocksize)) - return sumtable[sum.digest()],self.sig.count - def _appendmatch(self,(offset,length)): - "append a match to delta" - # if last was a match that can be extended, extend it - if type(self.last)==TupleType and self.last[0]+self.last[1]==offset: - self.last=(self.last[0],self.last[1]+length) + def flush_input(str, set=None): + if not set: + while msvcrt.kbhit(): + ch = msvcrt.getch() + if ch == '\xff': + print("msvcrt is broken, this is weird.") + sys.exit(0) + return input(str) else: - # else appendflush the last value - self._appendflush(R_SYNC_FLUSH) - # make this match the new last - self.last=(offset,length) - # increment pos and compress the matched data for context - self.pos=self.pos+length - self._compress() - def _appendmiss(self,length): - "append a miss to delta" - if type(self.last)!=StringType: - # if last was not a miss, appendflush the last value - self._appendflush(R_SYNC_FLUSH) - # make this miss the new last - self.last="" - # increment pos and compress if greater than blocksize - self.pos=self.pos+length - #if self.pos >= self.blocksize: - # self.last=self.last+self._compress() - def _appendflush(self,mode=R_FINISH): - "append a flush to delta" - if type(self.last)==StringType: - self.delta.append(self.last+self._flush(mode)) - elif self.last: - self.delta.append(self.last) - self._flush(mode) - self.last=None - def calcdelta(self,newdata): - "incrementaly calculates and returns a delta list" - self.data=self.data+newdata - while self.pos+self.blocksize 0: + os.read(sys.stdin.fileno(), 4096) + return input(str) else: - inserts=inserts+1 - insert_length=insert_length + len(i) - return """delta stats -segments: %i -matches : %i %i -inserts : %i %i -""" % (len(delta),matches,match_length,inserts,insert_length) + return set -if __name__ == "__main__": - import os - from sys import argv,stdin,stdout,stderr,exit - def openarg(argno,mode='rb'): - if (len(argv) <= argno) or (argv[argno] == '-'): - if 'r' in mode: return stdin - return stdout - return open(argv[argno],mode) - - if len(argv)>=2 and argv[1]=="signature": - oldfile,sigfile=openarg(2,'rb'),openarg(3,'wb') - stats=filesig(oldfile,sigfile,1024) - stderr.write(str(stats)) - elif len(argv)>=3 and argv[1]=="rdelta": - sigfile,newfile,diffile=openarg(2,'rb'),openarg(3,'rb'),openarg(4,'wb') - stats=filerdelta(sigfile,newfile,diffile) - stderr.write(str(stats)) - elif len(argv)>=3 and argv[1]=="xdelta": - oldfile,newfile,diffile=openarg(2,'rb'),openarg(3,'rb'),openarg(4,'wb') - stats=filexdelta(oldfile,newfile,diffile) - stderr.write(str(stats)) - elif len(argv)>=3 and argv[1]=="patch": - oldfile,diffile,newfile=openarg(2,'rb'),openarg(3,'rb'),openarg(4,'wb') - stats=filepatch(oldfile,diffile,newfile) - stderr.write(str(stats)) +def compare(fa, fb, options_input=[]): + if isfile(fa) == isfile(fb): + if isdir(fa): + walktree(fa, fb, options_input) + elif isfile(fa): + if getsize(fa) != getsize(fb) \ + or int(getmtime(fa)) != int(getmtime(fb)): + log.info(str((fa, ': size=', getsize(fa), 'mtime=', + time.asctime(time.localtime(getmtime(fa)))))) + log.info(str((fb, ': size=', getsize(fb), 'mtime=', + time.asctime(time.localtime(getmtime(fb)))))) + if getmtime(fa) > getmtime(fb): + act = '>' + else: + act = '<' + + set = [i for i in options_input if i in [">", "<"]][0] + + s = flush_input('What to do?(>,<,r,n)[' + act + ']', set=set) + if len(s) > 0: + act = s[0] + if act == '>': + shutil.copy2(fa, fb) + elif act == '<': + shutil.copy2(fb, fa) + elif act == 'r': + if isdir(fa): + shutil.rmtree(fa) + elif isfile(fa): + os.remove(fa) + else: + log.info(str(('Remove: Skipping', fa))) + if isdir(fb): + shutil.rmtree(fb) + elif isfile(fb): + os.remove(fb) + else: + log.info(str(('Remove: Skipping', fb))) + + else: + log.debug(str(('Compare: Skipping non-dir and non-file', fa))) else: - print """ -Usage: - %s signature [ []] - ... generates signature file from - - %s rdelta [ []] - ... generates rdelta file for from - - %s xdelta [ []] - ... generates xdelta file for from - - %s patch [ []] - ... applies delta file to to generate - -Where file parameters ommitted or specified as '-' indicate standard -input or output as appropriate. -""" % ((os.path.basename(argv[0]),) * 4) - exit(1) + log.error(str(('Error:', fa, ',', fb, 'have different file type'))) + + +def copy(fa, fb, options_input=[]): + set = [i for i in options_input if i in ["y"]][0] + s = flush_input('Copy ' + fa + ' to another side?(r,y,n)[y]', set=set) + if len(s) > 0: + act = s[0] + else: + act = 'y' + if act == 'y': + if isdir(fa): + shutil.copytree(fa, fb) + elif isfile(fa): + shutil.copy2(fa, fb) + else: + log.debug(str(('Copy: Skipping ', fa))) + elif act == 'r': + if isdir(fa): + shutil.rmtree(fa) + elif isfile(fa): + os.remove(fa) + else: + log.debug(str(('Remove: Skipping ', fa))) + + +stoentry = [] +tarentry = [] + + +def walktree(source, target, options_input=[]): + srclist = os.listdir(source) + tarlist = os.listdir(target) + if '!sync' in srclist: + return + if '!sync' in tarlist: + return + # files in source dir... + for f in srclist: + if f in ignoreFiles: + continue + spath = join(source, f) + tpath = join(target, f) + if spath in ignorePaths: + continue + if spath in stoentry: + # just in case target also have this one + if f in tarlist: + del tarlist[tarlist.index(f)] + continue + + # if also exists in target dir + if f in tarlist: + del tarlist[tarlist.index(f)] + compare(spath, tpath, options_input) + + # exists in source dir only + else: + copy(spath, tpath, options_input) + + # exists in target dir only + set = [i for i in options_input if i in ["<"]] + + for f in tarlist: + if f in ignoreFiles: + continue + spath = join(source, f) + tpath = join(target, f) + if tpath in ignorePaths: + continue + if tpath in tarentry: + continue + if set: + copy(tpath, spath, options_input) + else: + print("REMOVING: {}".format(f)) + if os.path.isdir(tpath): + shutil.rmtree(tpath) + else: + os.remove(tpath) + print("REMOVING: {}".format(f)) + + +if __name__ == '__main__': + stoconf = configparser.RawConfigParser() + tarconf = configparser.RawConfigParser() + stoconf.read("pySync.ini") + tarconf.read(expanduser("~/.pysync")) + stoname = stoconf.sections()[0] + tarname = tarconf.sections()[0] + + # calculate storage's base folder + if stoconf.has_option(stoname, 'BASE'): + stobase = abspath(stoconf.get(stoname, 'BASE')) + stoconf.remove_option(stoname, 'BASE') + else: + stobase = os.getcwd() + + # same, for target's base folder + if tarconf.has_option(tarname, 'BASE'): + tarbase = abspath(tarconf.get(tarname, 'BASE')) + tarconf.remove_option(tarname, 'BASE') + else: + tarbase = expanduser('~/') + + print("Syncing between", stoname, "and", tarname) + sto_content = {x: realpath(join(stobase, stoconf.get(stoname, x))) + for x in stoconf.options(stoname)} + tar_content = {x: realpath(join(tarbase, tarconf.get(tarname, x))) + for x in tarconf.options(tarname)} + stoentry = [sto_content[x] for x in sto_content] + tarentry = [tar_content[x] for x in tar_content] + + for folder in sto_content: + if folder in tar_content: + print('Processing', folder) + walktree(sto_content[folder], tar_content[folder], options_input) + print("Done.") From 12bea639ebd5a0a9dbaed87bad255faeed83ed89 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 1 Apr 2020 17:25:43 +0200 Subject: [PATCH 194/327] preserve mb file type during publish --- pype/plugins/maya/publish/collect_scene.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/plugins/maya/publish/collect_scene.py b/pype/plugins/maya/publish/collect_scene.py index 089019f2d3..3f5760fd7b 100644 --- a/pype/plugins/maya/publish/collect_scene.py +++ b/pype/plugins/maya/publish/collect_scene.py @@ -1,5 +1,3 @@ -from maya import cmds - import pyblish.api import avalon.api import os @@ -42,8 +40,8 @@ class CollectMayaScene(pyblish.api.ContextPlugin): }) data['representations'] = [{ - 'name': 'ma', - 'ext': 'ma', + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), 'files': file, "stagingDir": folder, }] From 17622b046d3e4fd814ad45d4f2e4cdd554d6a39a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 1 Apr 2020 19:16:54 +0200 Subject: [PATCH 195/327] fixed camera options --- pype/plugins/maya/publish/extract_playblast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index cc307d6afc..579712018c 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -58,9 +58,9 @@ class ExtractPlayblast(pype.api.Extractor): preset['compression'] = "png" preset['start_frame'] = start preset['end_frame'] = end - preset['camera_options'] = { - "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), - } + camera_option = preset.get("camera_option", {}) + camera_option["depthOfField"] = cmds.getAttr( + "{0}.depthOfField".format(camera)) stagingdir = self.staging_dir(instance) filename = "{0}".format(instance.name) From 70ef1d8ebcb130321ad70eb75caaf5808a09041b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Apr 2020 22:25:14 +0200 Subject: [PATCH 196/327] feat(premiere): extension package.json for node_modules --- .../extensions/com.pype.avalon/package.json | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 pype/premiere/extensions/com.pype.avalon/package.json diff --git a/pype/premiere/extensions/com.pype.avalon/package.json b/pype/premiere/extensions/com.pype.avalon/package.json new file mode 100644 index 0000000000..7f53bcf2b7 --- /dev/null +++ b/pype/premiere/extensions/com.pype.avalon/package.json @@ -0,0 +1,30 @@ +{ + "name": "com.pype.avalon", + "version": "1.0.0", + "description": "pype avalon integration", + "main": "CSXS\\manifest.xml", + "dependencies": { + "decompress-zip": "^0.2.2", + "fs-extra": "^9.0.0", + "fs": "^0.0.1-security", + "https": "^1.0.0", + "jsonfile": "^6.0.1", + "junk": "^3.1.0", + "node-timecodes": "^2.5.0", + "opn": "^6.0.0", + "os": "^0.1.1", + "path": "^0.12.7", + "process": "^0.11.10", + "pure-uuid": "^1.6.0", + "rimraf": "^3.0.2", + "url": "^0.11.0", + "walk": "^2.3.14", + "xml2js": "^0.4.23" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} From 124e61a847bc7939a362fb8fac89b46df518e183 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Apr 2020 22:25:49 +0200 Subject: [PATCH 197/327] feat(premiere): wip wrapper --- pype/hooks/premiere/prelaunch.py | 17 ++- pype/premiere/__init__.py | 174 ++++++------------------ pype/premiere/lib.py | 226 ++++++++++++++++++------------- 3 files changed, 183 insertions(+), 234 deletions(-) diff --git a/pype/hooks/premiere/prelaunch.py b/pype/hooks/premiere/prelaunch.py index 845ee63fb7..e7f36e9657 100644 --- a/pype/hooks/premiere/prelaunch.py +++ b/pype/hooks/premiere/prelaunch.py @@ -1,11 +1,8 @@ -import logging import os - +import traceback from pype.lib import PypeHook from pypeapp import Logger -log = logging.getLogger(__name__) - class PremierePrelaunch(PypeHook): """ @@ -26,11 +23,16 @@ class PremierePrelaunch(PypeHook): def execute(self, *args, env: dict = None) -> bool: if not env: env = os.environ + + EXTENSIONS_CACHE_PATH = env.get("EXTENSIONS_CACHE_PATH", None) + self.log.debug( + "_ EXTENSIONS_CACHE_PATH: `{}`".format(EXTENSIONS_CACHE_PATH)) asset = env["AVALON_ASSET"] task = env["AVALON_TASK"] workdir = env["AVALON_WORKDIR"] project_name = f"{asset}_{task}" + import importlib import avalon.api import pype.premiere avalon.api.install(pype.premiere) @@ -40,13 +42,14 @@ class PremierePrelaunch(PypeHook): __import__("pyblish") except ImportError as e: - print traceback.format_exc() + print(traceback.format_exc()) print("pyblish: Could not load integration: %s " % e) else: # Setup integration - import pype.premiere.lib - pype.premiere.lib.setup() + from pype.premiere import lib as prlib + importlib.reload(prlib) + prlib.setup(env) self.log.debug("_ self.signature: `{}`".format(self.signature)) self.log.debug("_ asset: `{}`".format(asset)) diff --git a/pype/premiere/__init__.py b/pype/premiere/__init__.py index 49912ef309..6e178e704a 100644 --- a/pype/premiere/__init__.py +++ b/pype/premiere/__init__.py @@ -1,167 +1,73 @@ import os -import sys -import shutil - -from pysync import walktree - from avalon import api as avalon from pyblish import api as pyblish -from app import api as app -from .. import api -import requests +from pypeapp import Logger -from .pipeline import ( - install, - uninstall, + +from .lib import ( + setup, reload_pipeline, - ls + ls, + LOAD_PATH, + INVENTORY_PATH, + CREATE_PATH, + PUBLISH_PATH, + PLUGINS_DIR ) __all__ = [ - "install", - "uninstall", + "setup", "reload_pipeline", "ls" ] -log = api.Logger.getLogger(__name__, "premiere") - -AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") -EXTENSIONS_PATH_LOCAL = os.getenv("EXTENSIONS_PATH", None) -EXTENSIONS_CACHE_PATH = os.getenv("EXTENSIONS_CACHE_PATH", None) -EXTENSIONS_PATH_REMOTE = os.path.join(os.path.dirname(__file__), "extensions") -PARENT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.dirname(PARENT_DIR) -PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") - -_clearing_cache = ["com.pype.rename", "com.pype.avalon"] - -PUBLISH_PATH = os.path.join( - PLUGINS_DIR, "premiere", "publish" -).replace("\\", "/") - -if os.getenv("PUBLISH_PATH", None): - os.environ["PUBLISH_PATH"] = os.pathsep.join( - os.environ["PUBLISH_PATH"].split(os.pathsep) + - [PUBLISH_PATH] - ) -else: - os.environ["PUBLISH_PATH"] = PUBLISH_PATH - -LOAD_PATH = os.path.join(PLUGINS_DIR, "premiere", "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "premiere", "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "premiere", "inventory") - -log.debug("_clearing_cache: {}".format(_clearing_cache)) - -def clearing_caches_ui(): - '''Before every start of premiere it will make sure there is not - outdated stuff in cep_cache dir''' - - for d in os.listdir(EXTENSIONS_CACHE_PATH): - match = [p for p in _clearing_cache - if str(p) in d] - - if match: - try: - path = os.path.normpath(os.path.join(EXTENSIONS_CACHE_PATH, d)) - log.info("Removing dir: {}".format(path)) - shutil.rmtree(path, ignore_errors=True) - except Exception as e: - log.debug("problem: {}".format(e)) - -def request_aport(url_path, data={}): - try: - api.add_tool_to_environment(["aport_0.1"]) - - ip = os.getenv("PICO_IP", None) - if ip and ip.startswith('http'): - ip = ip.replace("http://", "") - - port = int(os.getenv("PICO_PORT", None)) - - url = "http://{0}:{1}{2}".format(ip, port, url_path) - req = requests.post(url, data=data).text - return req - - except Exception as e: - api.message(title="Premiere Aport Server", - message="Before you can run Premiere, start Aport Server. \n Error: {}".format( - e), - level="critical") - - -def extensions_sync(): - # import time - process_pairs = list() - # get extensions dir in pype.premiere.extensions - # build dir path to premiere cep extensions - for name in os.listdir(EXTENSIONS_PATH_REMOTE): - print(name) - src = os.path.join(EXTENSIONS_PATH_REMOTE, name) - dst = os.path.join(EXTENSIONS_PATH_LOCAL, name) - process_pairs.append((name, src, dst)) - - # synchronize all extensions - for name, src, dst in process_pairs: - if not os.path.exists(dst): - os.makedirs(dst, mode=0o777) - walktree(source=src, target=dst, options_input=["y", ">"]) - log.info("Extension {0} from `{1}` coppied to `{2}`".format( - name, src, dst - )) - # time.sleep(10) - return +log = Logger().get_logger(__name__, "premiere") def install(): + """Install Premiere-specific functionality of avalon-core. - log.info("Registering Premiera plug-ins..") - reg_paths = request_aport("/api/register_plugin_path", - {"publish_path": PUBLISH_PATH}) + This is where you install menus and register families, data + and loaders into Premiere. + + It is called automatically when installing via `api.install(premiere)`. + + See the Maya equivalent for inspiration on how to implement this. + + """ # Disable all families except for the ones we explicitly want to see family_states = [ "imagesequence", "mov" - ] avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states - # load data from templates - api.load_data_from_templates() + log.info("pype.premiere installed") - # remove cep_cache from user temp dir - clearing_caches_ui() + pyblish.register_host("premiere") + pyblish.register_plugin_path(PUBLISH_PATH) + log.info("Registering Premiera plug-ins..") - # synchronize extensions - extensions_sync() - message = "The Pype extension has been installed. " \ - "\nThe following publishing paths has been registered: " \ - "\n\n{}".format( - reg_paths) - - api.message(title="pyblish_paths", message=message, level="info") - - # launching premiere - exe = r"C:\Program Files\Adobe\Adobe Premiere Pro CC 2019\Adobe Premiere Pro.exe".replace( - "\\", "/") - - log.info("____path exists: {}".format(os.path.exists(exe))) - - app.forward(args=[exe], - silent=False, - cwd=os.getcwd(), - env=dict(os.environ), - shell=None) + avalon.register_plugin_path(avalon.Loader, LOAD_PATH) + avalon.register_plugin_path(avalon.Creator, CREATE_PATH) def uninstall(): - log.info("Deregistering Premiera plug-ins..") + """Uninstall all tha was installed + + This is where you undo everything that was done in `install()`. + That means, removing menus, deregistering families and data + and everything. It should be as though `install()` was never run, + because odds are calling this function means the user is interested + in re-installing shortly afterwards. If, for example, he has been + modifying the menu or registered families. + + """ + pyblish.deregister_host("premiere") pyblish.deregister_plugin_path(PUBLISH_PATH) + log.info("Deregistering Premiera plug-ins..") + avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) - - # reset data from templates - api.reset_data_from_templates() diff --git a/pype/premiere/lib.py b/pype/premiere/lib.py index 154c55bb04..628002f6f7 100644 --- a/pype/premiere/lib.py +++ b/pype/premiere/lib.py @@ -1,14 +1,49 @@ import os -import importlib -from pyblish import api as pyblish +import sys +import shutil +import json +from pysync import walktree +import requests + from avalon import api -import logging +from pype.widgets.message_window import message +from pypeapp import Logger -log = logging.getLogger(__name__) +log = Logger().get_logger(__name__, "premiere") + +self = sys.modules[__name__] +self._has_been_setup = False +self._registered_gui = None AVALON_CONFIG = os.environ["AVALON_CONFIG"] +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +self.EXTENSIONS_PATH_REMOTE = os.path.join(PARENT_DIR, "extensions") +self.EXTENSIONS_PATH_LOCAL = None +self.EXTENSIONS_CACHE_PATH = None + +self.LOAD_PATH = os.path.join(PLUGINS_DIR, "premiere", "load") +self.CREATE_PATH = os.path.join(PLUGINS_DIR, "premiere", "create") +self.INVENTORY_PATH = os.path.join(PLUGINS_DIR, "premiere", "inventory") + +self.PUBLISH_PATH = os.path.join( + PLUGINS_DIR, "premiere", "publish" +).replace("\\", "/") + +if os.getenv("PUBLISH_PATH", None): + os.environ["PUBLISH_PATH"] = os.pathsep.join( + os.environ["PUBLISH_PATH"].split(os.pathsep) + + [self.PUBLISH_PATH] + ) +else: + os.environ["PUBLISH_PATH"] = self.PUBLISH_PATH + +_clearing_cache = ["com.pype.rename", "com.pype.avalon"] + def ls(): pass @@ -31,107 +66,112 @@ def reload_pipeline(): "avalon.api", "avalon.tools", - "avalon.tools.loader.app", - "avalon.tools.creator.app", - "avalon.tools.manager.app", - - "avalon.premiere", - "avalon.premiere.pipeline", - "{}".format(AVALON_CONFIG) + "{}".format(AVALON_CONFIG), + "{}.premiere".format(AVALON_CONFIG), + "{}.premiere.lib".format(AVALON_CONFIG) ): log.info("Reloading module: {}...".format(module)) - module = importlib.import_module(module) - reload(module) + try: + module = importlib.import_module(module) + reload(module) + except Exception as e: + log.warning("Cannot reload module: {}".format(e)) + importlib.reload(module) import avalon.premiere api.install(avalon.premiere) -def install(config): - """Install Premiere-specific functionality of avalon-core. - - This is where you install menus and register families, data - and loaders into Premiere. - - It is called automatically when installing via `api.install(premiere)`. - - See the Maya equivalent for inspiration on how to implement this. - +def setup(env=None): + """ Running wrapper """ + if not env: + env = os.environ - pyblish.register_host("premiere") - # Trigger install on the config's "premiere" package - config = find_host_config(config) + self.EXTENSIONS_PATH_LOCAL = env["EXTENSIONS_PATH"] + self.EXTENSIONS_CACHE_PATH = env["EXTENSIONS_CACHE_PATH"] - if hasattr(config, "install"): - config.install() + log.info("Registering Premiera plug-ins..") + if not test_rest_api_server(): + return - log.info("config.premiere installed") + # remove cep_cache from user temp dir + clearing_caches_ui() + + # synchronize extensions + extensions_sync() + + log.info("Premiere Pype wrapper has been installed") -def find_host_config(config): +def extensions_sync(): + # import time + process_pairs = list() + # get extensions dir in pype.premiere.extensions + # build dir path to premiere cep extensions + + for name in os.listdir(self.EXTENSIONS_PATH_REMOTE): + log.debug("> name: {}".format(name)) + src = os.path.join(self.EXTENSIONS_PATH_REMOTE, name) + dst = os.path.join(self.EXTENSIONS_PATH_LOCAL, name) + process_pairs.append((name, src, dst)) + + # synchronize all extensions + for name, src, dst in process_pairs: + if not os.path.exists(dst): + os.makedirs(dst, mode=0o777) + walktree(source=src, target=dst, options_input=["y", ">"]) + log.info("Extension {0} from `{1}` coppied to `{2}`".format( + name, src, dst + )) + # time.sleep(10) + return + + +def clearing_caches_ui(): + '''Before every start of premiere it will make sure there is not + outdated stuff in cep_cache dir''' + + if not os.path.isdir(self.EXTENSIONS_CACHE_PATH): + os.makedirs(self.EXTENSIONS_CACHE_PATH, mode=0o777) + log.info("Created dir: {}".format(self.EXTENSIONS_CACHE_PATH)) + + for d in os.listdir(self.EXTENSIONS_CACHE_PATH): + match = [p for p in _clearing_cache + if str(p) in d] + + if match: + try: + path = os.path.normpath( + os.path.join(self.EXTENSIONS_CACHE_PATH, d)) + log.info("Removing dir: {}".format(path)) + shutil.rmtree(path, ignore_errors=True) + except Exception as e: + log.debug("problem: {}".format(e)) + + +def test_rest_api_server(): + from pprint import pformat + rest_url = os.getenv("PYPE_REST_API_URL") + project_name = "{AVALON_PROJECT}".format(**dict(os.environ)) + URL = "/".join((rest_url, + "avalon/projects", + project_name)) + log.debug("__ URL: {}".format(URL)) try: - config = importlib.import_module(config.__name__ + ".premiere") - except ImportError as exc: - if str(exc) != "No module name {}".format( - config.__name__ + ".premiere"): - raise - config = None + req = requests.get(URL, data={}).text + req_json = json.loads(req) + # log.debug("_ req_json: {}".format(pformat(req_json))) + log.debug("__ projectName: {}".format(req_json["data"]["name"])) + assert req_json["data"]["name"] == project_name, ( + "Project data from Rest API server not correct") + return True - return config - - -def uninstall(config): - """Uninstall all tha was installed - - This is where you undo everything that was done in `install()`. - That means, removing menus, deregistering families and data - and everything. It should be as though `install()` was never run, - because odds are calling this function means the user is interested - in re-installing shortly afterwards. If, for example, he has been - modifying the menu or registered families. - - """ - config = find_host_config(config) - if hasattr(config, "uninstall"): - config.uninstall() - - pyblish.deregister_host("premiere") - - -def get_anatomy(**kwarg): - return pype.Anatomy - - -def get_dataflow(**kwarg): - log.info(kwarg) - host = kwarg.get("host", "premiere") - cls = kwarg.get("class", None) - preset = kwarg.get("preset", None) - assert any([host, cls]), log.error("premiera.lib.get_dataflow():" - "Missing mandatory kwargs `host`, `cls`") - - pr_dataflow = getattr(pype.Dataflow, str(host), None) - pr_dataflow_node = getattr(pr_dataflow.nodes, str(cls), None) - if preset: - pr_dataflow_node = getattr(pr_dataflow_node, str(preset), None) - - log.info("Dataflow: {}".format(pr_dataflow_node)) - return pr_dataflow_node - - -def get_colorspace(**kwarg): - log.info(kwarg) - host = kwarg.get("host", "premiere") - cls = kwarg.get("class", None) - preset = kwarg.get("preset", None) - assert any([host, cls]), log.error("premiera.templates.get_colorspace():" - "Missing mandatory kwargs `host`, `cls`") - - pr_colorspace = getattr(pype.Colorspace, str(host), None) - pr_colorspace_node = getattr(pr_colorspace, str(cls), None) - if preset: - pr_colorspace_node = getattr(pr_colorspace_node, str(preset), None) - - log.info("Colorspace: {}".format(pr_colorspace_node)) - return pr_colorspace_node + except Exception as e: + message(title="Pype Rest API static server is not running ", + message=("Before you can run Premiere, make sure " + "the system Tray Pype icon is running and " + "submenu `service` with name `Rest API` is " + "with green icon." + "\n Error: {}".format(e)), + level="critical") From a0ab0fea8808ec5ab07d0d86834bba401119fe08 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 2 Apr 2020 15:03:40 +0200 Subject: [PATCH 198/327] feat(premiere): update static page --- pype/aport/io_nonsingleton.py | 432 -- pype/hooks/premiere/prelaunch.py | 19 +- pype/premiere/static_ppro/css/avalon.min.css | 3 + .../static_ppro/css/avalon.min.css.map | 9 + pype/premiere/static_ppro/css/avalon.scss | 17 + .../static_ppro/css/bootstrap.min.css | 7 + .../static_ppro/css/bootstrap.min.css.map | 1 + pype/premiere/static_ppro/img/blender.png | Bin 0 -> 51122 bytes pype/premiere/static_ppro/index.html | 162 + pype/premiere/static_ppro/js/avalon.js | 367 ++ pype/premiere/static_ppro/js/build.js | 4862 +++++++++++++++++ pype/premiere/static_ppro/js/pico_client.js | 75 + .../static_ppro/js/vendor/CSInterface-8.js | 1193 ++++ .../static_ppro/js/vendor/bootstrap.min.js | 7 + .../js/vendor/bootstrap.min.js.map | 1 + .../static_ppro/js/vendor/jquery-3.3.1.min.js | 2 + pype/premiere/static_ppro/js/vendor/json2.js | 489 ++ .../static_ppro/js/vendor/popper.min.js | 5 + 18 files changed, 7215 insertions(+), 436 deletions(-) delete mode 100644 pype/aport/io_nonsingleton.py create mode 100644 pype/premiere/static_ppro/css/avalon.min.css create mode 100644 pype/premiere/static_ppro/css/avalon.min.css.map create mode 100644 pype/premiere/static_ppro/css/avalon.scss create mode 100644 pype/premiere/static_ppro/css/bootstrap.min.css create mode 100644 pype/premiere/static_ppro/css/bootstrap.min.css.map create mode 100644 pype/premiere/static_ppro/img/blender.png create mode 100644 pype/premiere/static_ppro/index.html create mode 100644 pype/premiere/static_ppro/js/avalon.js create mode 100644 pype/premiere/static_ppro/js/build.js create mode 100644 pype/premiere/static_ppro/js/pico_client.js create mode 100644 pype/premiere/static_ppro/js/vendor/CSInterface-8.js create mode 100644 pype/premiere/static_ppro/js/vendor/bootstrap.min.js create mode 100644 pype/premiere/static_ppro/js/vendor/bootstrap.min.js.map create mode 100644 pype/premiere/static_ppro/js/vendor/jquery-3.3.1.min.js create mode 100644 pype/premiere/static_ppro/js/vendor/json2.js create mode 100644 pype/premiere/static_ppro/js/vendor/popper.min.js diff --git a/pype/aport/io_nonsingleton.py b/pype/aport/io_nonsingleton.py deleted file mode 100644 index ddda21a570..0000000000 --- a/pype/aport/io_nonsingleton.py +++ /dev/null @@ -1,432 +0,0 @@ -""" -Wrapper around interactions with the database - -Copy of io module in avalon-core. - - In this case not working as singleton with api.Session! -""" - -import os -import time -import errno -import shutil -import logging -import tempfile -import functools -import contextlib - -from avalon import schema -import requests - -# Third-party dependencies -import pymongo - - -def auto_reconnect(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(*args, **kwargs): - object = args[0] - for retry in range(3): - try: - return func(*args, **kwargs) - except pymongo.errors.AutoReconnect: - object.log.error("Reconnecting..") - time.sleep(0.1) - else: - raise - - return decorated - - -class DbConnector(object): - - log = logging.getLogger(__name__) - - def __init__(self): - self.Session = {} - self._mongo_client = None - self._sentry_client = None - self._sentry_logging_handler = None - self._database = None - self._is_installed = False - - def install(self): - """Establish a persistent connection to the database""" - if self._is_installed: - return - - logging.basicConfig() - self.Session.update(self._from_environment()) - - timeout = int(self.Session["AVALON_TIMEOUT"]) - self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) - - for retry in range(3): - try: - t1 = time.time() - self._mongo_client.server_info() - - except Exception: - self.log.error("Retrying..") - time.sleep(1) - timeout *= 1.5 - - else: - break - - else: - raise IOError( - "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self.Session["AVALON_MONGO"], timeout)) - - self.log.info("Connected to %s, delay %.3f s" % ( - self.Session["AVALON_MONGO"], time.time() - t1)) - - self._install_sentry() - - self._database = self._mongo_client[self.Session["AVALON_DB"]] - self._is_installed = True - - def _install_sentry(self): - if "AVALON_SENTRY" not in self.Session: - return - - try: - from raven import Client - from raven.handlers.logging import SentryHandler - from raven.conf import setup_logging - except ImportError: - # Note: There was a Sentry address in this Session - return self.log.warning("Sentry disabled, raven not installed") - - client = Client(self.Session["AVALON_SENTRY"]) - - # Transmit log messages to Sentry - handler = SentryHandler(client) - handler.setLevel(logging.WARNING) - - setup_logging(handler) - - self._sentry_client = client - self._sentry_logging_handler = handler - self.log.info( - "Connected to Sentry @ %s" % self.Session["AVALON_SENTRY"] - ) - - def _from_environment(self): - Session = { - item[0]: os.getenv(item[0], item[1]) - for item in ( - # Root directory of projects on disk - ("AVALON_PROJECTS", None), - - # Name of current Project - ("AVALON_PROJECT", ""), - - # Name of current Asset - ("AVALON_ASSET", ""), - - # Name of current silo - ("AVALON_SILO", ""), - - # Name of current task - ("AVALON_TASK", None), - - # Name of current app - ("AVALON_APP", None), - - # Path to working directory - ("AVALON_WORKDIR", None), - - # Name of current Config - # TODO(marcus): Establish a suitable default config - ("AVALON_CONFIG", "no_config"), - - # Name of Avalon in graphical user interfaces - # Use this to customise the visual appearance of Avalon - # to better integrate with your surrounding pipeline - ("AVALON_LABEL", "Avalon"), - - # Used during any connections to the outside world - ("AVALON_TIMEOUT", "1000"), - - # Address to Asset Database - ("AVALON_MONGO", "mongodb://localhost:27017"), - - # Name of database used in MongoDB - ("AVALON_DB", "avalon"), - - # Address to Sentry - ("AVALON_SENTRY", None), - - # Address to Deadline Web Service - # E.g. http://192.167.0.1:8082 - ("AVALON_DEADLINE", None), - - # Enable features not necessarily stable. The user's own risk - ("AVALON_EARLY_ADOPTER", None), - - # Address of central asset repository, contains - # the following interface: - # /upload - # /download - # /manager (optional) - ("AVALON_LOCATION", "http://127.0.0.1"), - - # Boolean of whether to upload published material - # to central asset repository - ("AVALON_UPLOAD", None), - - # Generic username and password - ("AVALON_USERNAME", "avalon"), - ("AVALON_PASSWORD", "secret"), - - # Unique identifier for instances in working files - ("AVALON_INSTANCE_ID", "avalon.instance"), - ("AVALON_CONTAINER_ID", "avalon.container"), - - # Enable debugging - ("AVALON_DEBUG", None), - - ) if os.getenv(item[0], item[1]) is not None - } - - Session["schema"] = "avalon-core:session-1.0" - try: - schema.validate(Session) - except schema.ValidationError as e: - # TODO(marcus): Make this mandatory - self.log.warning(e) - - return Session - - def uninstall(self): - """Close any connection to the database""" - try: - self._mongo_client.close() - except AttributeError: - pass - - self._mongo_client = None - self._database = None - self._is_installed = False - - def active_project(self): - """Return the name of the active project""" - return self.Session["AVALON_PROJECT"] - - def activate_project(self, project_name): - self.Session["AVALON_PROJECT"] = project_name - - def projects(self): - """List available projects - - Returns: - list of project documents - - """ - - collection_names = self.collections() - for project in collection_names: - if project in ("system.indexes",): - continue - - # Each collection will have exactly one project document - document = self.find_project(project) - - if document is not None: - yield document - - def locate(self, path): - """Traverse a hierarchy from top-to-bottom - - Example: - representation = locate(["hulk", "Bruce", "modelDefault", 1, "ma"]) - - Returns: - representation (ObjectId) - - """ - - components = zip( - ("project", "asset", "subset", "version", "representation"), - path - ) - - parent = None - for type_, name in components: - latest = (type_ == "version") and name in (None, -1) - - try: - if latest: - parent = self.find_one( - filter={ - "type": type_, - "parent": parent - }, - projection={"_id": 1}, - sort=[("name", -1)] - )["_id"] - else: - parent = self.find_one( - filter={ - "type": type_, - "name": name, - "parent": parent - }, - projection={"_id": 1}, - )["_id"] - - except TypeError: - return None - - return parent - - @auto_reconnect - def collections(self): - return self._database.collection_names() - - @auto_reconnect - def find_project(self, project): - return self._database[project].find_one({"type": "project"}) - - @auto_reconnect - def insert_one(self, item): - assert isinstance(item, dict), "item must be of type " - schema.validate(item) - return self._database[self.Session["AVALON_PROJECT"]].insert_one(item) - - @auto_reconnect - def insert_many(self, items, ordered=True): - # check if all items are valid - assert isinstance(items, list), "`items` must be of type " - for item in items: - assert isinstance(item, dict), "`item` must be of type " - schema.validate(item) - - return self._database[self.Session["AVALON_PROJECT"]].insert_many( - items, - ordered=ordered) - - @auto_reconnect - def find(self, filter, projection=None, sort=None): - return self._database[self.Session["AVALON_PROJECT"]].find( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def find_one(self, filter, projection=None, sort=None): - assert isinstance(filter, dict), "filter must be " - return self._database[self.Session["AVALON_PROJECT"]].find_one( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def save(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].save( - *args, **kwargs) - - @auto_reconnect - def replace_one(self, filter, replacement): - return self._database[self.Session["AVALON_PROJECT"]].replace_one( - filter, replacement) - - @auto_reconnect - def update_many(self, filter, update): - return self._database[self.Session["AVALON_PROJECT"]].update_many( - filter, update) - - @auto_reconnect - def distinct(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].distinct( - *args, **kwargs) - - @auto_reconnect - def drop(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].drop( - *args, **kwargs) - - @auto_reconnect - def delete_many(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].delete_many( - *args, **kwargs) - - def parenthood(self, document): - assert document is not None, "This is a bug" - - parents = list() - - while document.get("parent") is not None: - document = self.find_one({"_id": document["parent"]}) - - if document is None: - break - - parents.append(document) - - return parents - - @contextlib.contextmanager - def tempdir(self): - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - shutil.rmtree(tempdir) - - def download(self, src, dst): - """Download `src` to `dst` - - Arguments: - src (str): URL to source file - dst (str): Absolute path to destination file - - Yields tuple (progress, error): - progress (int): Between 0-100 - error (Exception): Any exception raised when first making connection - - """ - - try: - response = requests.get( - src, - stream=True, - auth=requests.auth.HTTPBasicAuth( - self.Session["AVALON_USERNAME"], - self.Session["AVALON_PASSWORD"] - ) - ) - except requests.ConnectionError as e: - yield None, e - return - - with self.tempdir() as dirname: - tmp = os.path.join(dirname, os.path.basename(src)) - - with open(tmp, "wb") as f: - total_length = response.headers.get("content-length") - - if total_length is None: # no content length header - f.write(response.content) - else: - downloaded = 0 - total_length = int(total_length) - for data in response.iter_content(chunk_size=4096): - downloaded += len(data) - f.write(data) - - yield int(100.0 * downloaded / total_length), None - - try: - os.makedirs(os.path.dirname(dst)) - except OSError as e: - # An already existing destination directory is fine. - if e.errno != errno.EEXIST: - raise - - shutil.copy(tmp, dst) diff --git a/pype/hooks/premiere/prelaunch.py b/pype/hooks/premiere/prelaunch.py index e7f36e9657..11ec637c17 100644 --- a/pype/hooks/premiere/prelaunch.py +++ b/pype/hooks/premiere/prelaunch.py @@ -21,12 +21,13 @@ class PremierePrelaunch(PypeHook): self.signature = "( {} )".format(self.__class__.__name__) def execute(self, *args, env: dict = None) -> bool: + from pype.services.rest_api.base_class import register_statics + if not env: env = os.environ - EXTENSIONS_CACHE_PATH = env.get("EXTENSIONS_CACHE_PATH", None) - self.log.debug( - "_ EXTENSIONS_CACHE_PATH: `{}`".format(EXTENSIONS_CACHE_PATH)) + PYPE_MODULE_ROOT = env.get("PYPE_MODULE_ROOT", None) + asset = env["AVALON_ASSET"] task = env["AVALON_TASK"] workdir = env["AVALON_WORKDIR"] @@ -46,7 +47,17 @@ class PremierePrelaunch(PypeHook): print("pyblish: Could not load integration: %s " % e) else: - # Setup integration + # start rest api static server + static_site_dir_path = os.path.join( + PYPE_MODULE_ROOT, + "pype", + "premiere", + "static_ppro").replace("\\", "/") + self.log.debug( + "_ static_site_dir_path: `{}`".format(static_site_dir_path)) + register_statics("/ppro", static_site_dir_path) + + # Premiere Setup integration from pype.premiere import lib as prlib importlib.reload(prlib) prlib.setup(env) diff --git a/pype/premiere/static_ppro/css/avalon.min.css b/pype/premiere/static_ppro/css/avalon.min.css new file mode 100644 index 0000000000..c0ac0d732b --- /dev/null +++ b/pype/premiere/static_ppro/css/avalon.min.css @@ -0,0 +1,3 @@ +body{background-color:#323238;color:#eeeeee}#output{background:#121212;color:#eeeeee;padding:2em;font-family:monospace;font-weight:bold;min-height:8em}.dark>.list-group-item{background:#454747} + +/*# sourceMappingURL=avalon.min.css.map */ \ No newline at end of file diff --git a/pype/premiere/static_ppro/css/avalon.min.css.map b/pype/premiere/static_ppro/css/avalon.min.css.map new file mode 100644 index 0000000000..2999d6cbe9 --- /dev/null +++ b/pype/premiere/static_ppro/css/avalon.min.css.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "file": "avalon.min.css", + "sources": [ + "avalon.scss" + ], + "names": [], + "mappings": "AAAA,AAAA,IAAI,AAAC,CACH,gBAAgB,CAAE,OAAO,CACzB,KAAK,CAAE,OAAO,CACf,AAED,AAAA,OAAO,AAAC,CACN,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,OAAO,CACd,OAAO,CAAE,GAAG,CACZ,WAAW,CAAE,SAAS,CACtB,WAAW,CAAE,IAAI,CACjB,UAAU,CAAE,GAAG,CAChB,AAED,AAAA,KAAK,CAAG,gBAAgB,AAAC,CACvB,UAAU,CAAE,OAAO,CACpB" +} \ No newline at end of file diff --git a/pype/premiere/static_ppro/css/avalon.scss b/pype/premiere/static_ppro/css/avalon.scss new file mode 100644 index 0000000000..cf06ece9be --- /dev/null +++ b/pype/premiere/static_ppro/css/avalon.scss @@ -0,0 +1,17 @@ +body { + background-color: #323238; + color: #eeeeee; +} + +#output { + background: #121212; + color: #eeeeee; + padding: 2em; + font-family: monospace; + font-weight: bold; + min-height: 8em; +} + +.dark > .list-group-item { + background: #454747; +} diff --git a/pype/premiere/static_ppro/css/bootstrap.min.css b/pype/premiere/static_ppro/css/bootstrap.min.css new file mode 100644 index 0000000000..e6b4977799 --- /dev/null +++ b/pype/premiere/static_ppro/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.2.1 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors + * Copyright 2011-2018 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:2.25rem;background-repeat:no-repeat;background-position:center right calc(2.25rem / 4);background-size:calc(2.25rem / 2) calc(2.25rem / 2);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e")}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:2.25rem;background-position:top calc(2.25rem / 4) right calc(2.25rem / 4)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:3.4375rem;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") no-repeat center right 1.75rem/1.125rem 1.125rem}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:2.25rem;background-repeat:no-repeat;background-position:center right calc(2.25rem / 4);background-size:calc(2.25rem / 2) calc(2.25rem / 2);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E")}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:2.25rem;background-position:top calc(2.25rem / 4) right calc(2.25rem / 4)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:3.4375rem;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") no-repeat center right 1.75rem/1.125rem 1.125rem}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-right{right:0;left:auto}}.dropdown-menu-left{right:auto;left:0}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:first-child{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.dropdown-item:last-child{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(2.875rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.8125rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;color:inherit;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card{overflow:hidden}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion .card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled){cursor:pointer}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);border-radius:.25rem;box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-dialog-centered::before{display:block;height:calc(100vh - (.5rem * 2));content:""}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-dialog-centered::before{height:calc(100vh - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media screen and (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-3by4::before{padding-top:133.333333%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/pype/premiere/static_ppro/css/bootstrap.min.css.map b/pype/premiere/static_ppro/css/bootstrap.min.css.map new file mode 100644 index 0000000000..5acf96bdd1 --- /dev/null +++ b/pype/premiere/static_ppro/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","bootstrap.css","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/mixins/_float.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/mixins/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v4.2.1 (https://getbootstrap.com/)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n",":root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n font-size: $font-size-base;\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Suppress the focus outline on elements that cannot be accessed via keyboard.\n// This prevents an unwanted focus outline from appearing around elements that\n// might still respond to pointer events.\n//\n// Credit: https://github.com/suitcss/base\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`
` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap v4.2.1 (https://getbootstrap.com/)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-family: inherit;\n font-weight: 500;\n line-height: 1.2;\n color: inherit;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-break: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n background-color: transparent;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table .table {\n background-color: #fff;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #212529;\n border-color: #32383e;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #212529;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #32383e;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::-webkit-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-moz-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.8125rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(2.875rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n -ms-flex-align: center;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: 2.25rem;\n background-repeat: no-repeat;\n background-position: center right calc(2.25rem / 4);\n background-size: calc(2.25rem / 2) calc(2.25rem / 2);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: 2.25rem;\n background-position: top calc(2.25rem / 4) right calc(2.25rem / 4);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: 3.4375rem;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") no-repeat center right 1.75rem/1.125rem 1.125rem;\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-select:valid ~ .valid-feedback,\n.was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:valid ~ .valid-feedback,\n.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback,\n.form-control-file.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: 2.25rem;\n background-repeat: no-repeat;\n background-position: center right calc(2.25rem / 4);\n background-size: calc(2.25rem / 2) calc(2.25rem / 2);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\");\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: 2.25rem;\n background-position: top calc(2.25rem / 4) right calc(2.25rem / 4);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: 3.4375rem;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\") no-repeat center right 1.75rem/1.125rem 1.125rem;\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-select:invalid ~ .invalid-feedback,\n.was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:invalid ~ .invalid-feedback,\n.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback,\n.form-control-file.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:first-child {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.dropdown-item:last-child {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: -ms-inline-flexbox;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: center;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: stretch;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n width: 1%;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: -ms-flexbox;\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(2.875rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.8125rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background-repeat: no-repeat;\n background-position: center center;\n background-size: 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n -webkit-transform: translateX(0.75rem);\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(128, 189, 255, 0.5);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n opacity: 0;\n}\n\n.custom-select-sm {\n height: calc(1.8125rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(2.875rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: 2.25rem;\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: calc(1rem + 0.4rem);\n padding: 0;\n background-color: transparent;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n appearance: none;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -moz-appearance: none;\n appearance: none;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n -ms-flex-preferred-size: 100%;\n flex-basis: 100%;\n -ms-flex-positive: 1;\n flex-grow: 1;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n color: inherit;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img {\n width: 100%;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img-top {\n width: 100%;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img-bottom {\n width: 100%;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n -ms-flex-direction: column;\n flex-direction: column;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n }\n .card-group > .card {\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:first-child {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-top,\n .card-group > .card:first-child .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-bottom,\n .card-group > .card:first-child .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:last-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-top,\n .card-group > .card:last-child .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-bottom,\n .card-group > .card:last-child .card-footer {\n border-bottom-left-radius: 0;\n }\n .card-group > .card:only-child {\n border-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-top,\n .card-group > .card:only-child .card-header {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-bottom,\n .card-group > .card:only-child .card-footer {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) {\n border-radius: 0;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer {\n border-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n -webkit-column-count: 3;\n -moz-column-count: 3;\n column-count: 3;\n -webkit-column-gap: 1.25rem;\n -moz-column-gap: 1.25rem;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion .card {\n overflow: hidden;\n}\n\n.accordion .card:not(:first-of-type) .card-header:first-child {\n border-radius: 0;\n}\n\n.accordion .card:not(:first-of-type):not(:last-of-type) {\n border-bottom: 0;\n border-radius: 0;\n}\n\n.accordion .card:first-of-type {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion .card:last-of-type {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion .card .card-header {\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: -ms-flexbox;\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 2;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-link:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 1;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: -ms-flexbox;\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-pack: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n -webkit-animation: progress-bar-stripes 1s linear infinite;\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n.media {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n}\n\n.media-body {\n -ms-flex: 1;\n flex: 1;\n}\n\n.list-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item:hover, .list-group-item:focus {\n z-index: 1;\n text-decoration: none;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-flush .list-group-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n\n.list-group-flush .list-group-item:last-child {\n margin-bottom: -1px;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n margin-bottom: 0;\n border-bottom: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 0.25rem;\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n opacity: 0;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;\n -webkit-transform: translate(0, -50px);\n transform: translate(0, -50px);\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n -webkit-transform: none;\n transform: none;\n}\n\n.modal-dialog-centered {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n min-height: calc(100% - (0.5rem * 2));\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - (0.5rem * 2));\n content: \"\";\n}\n\n.modal-content {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #e9ecef;\n border-top-left-radius: 0.3rem;\n border-top-right-radius: 0.3rem;\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: end;\n justify-content: flex-end;\n padding: 1rem;\n border-top: 1px solid #e9ecef;\n border-bottom-right-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.modal-footer > :not(:first-child) {\n margin-left: .25rem;\n}\n\n.modal-footer > :not(:last-child) {\n margin-right: .25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-centered {\n min-height: calc(100% - (1.75rem * 2));\n }\n .modal-dialog-centered::before {\n height: calc(100vh - (1.75rem * 2));\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top .arrow, .bs-popover-auto[x-placement^=\"top\"] .arrow {\n bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before,\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0;\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before {\n bottom: 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n bottom: 1px;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right .arrow, .bs-popover-auto[x-placement^=\"right\"] .arrow {\n left: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before,\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0.5rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before {\n left: 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n left: 1px;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom .arrow, .bs-popover-auto[x-placement^=\"bottom\"] .arrow {\n top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before,\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n border-width: 0 0.5rem 0.5rem 0.5rem;\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before {\n top: 0;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n top: 1px;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left .arrow, .bs-popover-auto[x-placement^=\"left\"] .arrow {\n right: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before,\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n border-width: 0.5rem 0 0.5rem 0.5rem;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before {\n right: 0;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n right: 1px;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n color: inherit;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n -ms-touch-action: pan-y;\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n transition: -webkit-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n -webkit-transform: translateX(100%);\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n -webkit-transform: translateX(-100%);\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n -webkit-transform: none;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: 0s 0.6s opacity;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: transparent no-repeat center center;\n background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n -ms-flex: 0 1 auto;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@-webkit-keyframes spinner-border {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes spinner-border {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n -webkit-animation: spinner-border .75s linear infinite;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@-webkit-keyframes spinner-grow {\n 0% {\n -webkit-transform: scale(0);\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n@keyframes spinner-grow {\n 0% {\n -webkit-transform: scale(0);\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n -webkit-animation: spinner-grow .75s linear infinite;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-3by4::before {\n padding-top: 133.333333%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n .sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*!\n * Bootstrap v4.2.1 (https://getbootstrap.com/)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-family: inherit;\n font-weight: 500;\n line-height: 1.2;\n color: inherit;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-break: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n background-color: transparent;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table .table {\n background-color: #fff;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #212529;\n border-color: #32383e;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #212529;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #32383e;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.8125rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(2.875rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: 2.25rem;\n background-repeat: no-repeat;\n background-position: center right calc(2.25rem / 4);\n background-size: calc(2.25rem / 2) calc(2.25rem / 2);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\");\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: 2.25rem;\n background-position: top calc(2.25rem / 4) right calc(2.25rem / 4);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: 3.4375rem;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e\") no-repeat center right 1.75rem/1.125rem 1.125rem;\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-select:valid ~ .valid-feedback,\n.was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:valid ~ .valid-feedback,\n.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback,\n.form-control-file.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: 2.25rem;\n background-repeat: no-repeat;\n background-position: center right calc(2.25rem / 4);\n background-size: calc(2.25rem / 2) calc(2.25rem / 2);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\");\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: 2.25rem;\n background-position: top calc(2.25rem / 4) right calc(2.25rem / 4);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: 3.4375rem;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px, url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E\") no-repeat center right 1.75rem/1.125rem 1.125rem;\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-select:invalid ~ .invalid-feedback,\n.was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:invalid ~ .invalid-feedback,\n.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback,\n.form-control-file.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:first-child {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.dropdown-item:last-child {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(2.875rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.8125rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background-repeat: no-repeat;\n background-position: center center;\n background-size: 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e\") no-repeat right 0.75rem center/8px 10px;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(128, 189, 255, 0.5);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n opacity: 0;\n}\n\n.custom-select-sm {\n height: calc(1.8125rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(2.875rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: 2.25rem;\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: calc(1rem + 0.4rem);\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n color: inherit;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img {\n width: 100%;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img-top {\n width: 100%;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img-bottom {\n width: 100%;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck {\n display: flex;\n flex-direction: column;\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n display: flex;\n flex: 1 0 0%;\n flex-direction: column;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group {\n display: flex;\n flex-direction: column;\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:first-child {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-top,\n .card-group > .card:first-child .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-bottom,\n .card-group > .card:first-child .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:last-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-top,\n .card-group > .card:last-child .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-bottom,\n .card-group > .card:last-child .card-footer {\n border-bottom-left-radius: 0;\n }\n .card-group > .card:only-child {\n border-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-top,\n .card-group > .card:only-child .card-header {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-bottom,\n .card-group > .card:only-child .card-footer {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) {\n border-radius: 0;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer {\n border-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion .card {\n overflow: hidden;\n}\n\n.accordion .card:not(:first-of-type) .card-header:first-child {\n border-radius: 0;\n}\n\n.accordion .card:not(:first-of-type):not(:last-of-type) {\n border-bottom: 0;\n border-radius: 0;\n}\n\n.accordion .card:first-of-type {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion .card:last-of-type {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion .card .card-header {\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 2;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-link:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 1;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item:hover, .list-group-item:focus {\n z-index: 1;\n text-decoration: none;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-flush .list-group-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n\n.list-group-flush .list-group-item:last-child {\n margin-bottom: -1px;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n margin-bottom: 0;\n border-bottom: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n appearance: none;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n border-radius: 0.25rem;\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n opacity: 0;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: none;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - (0.5rem * 2));\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - (0.5rem * 2));\n content: \"\";\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #e9ecef;\n border-top-left-radius: 0.3rem;\n border-top-right-radius: 0.3rem;\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n padding: 1rem;\n border-top: 1px solid #e9ecef;\n border-bottom-right-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.modal-footer > :not(:first-child) {\n margin-left: .25rem;\n}\n\n.modal-footer > :not(:last-child) {\n margin-right: .25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-centered {\n min-height: calc(100% - (1.75rem * 2));\n }\n .modal-dialog-centered::before {\n height: calc(100vh - (1.75rem * 2));\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top .arrow, .bs-popover-auto[x-placement^=\"top\"] .arrow {\n bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before,\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0;\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before {\n bottom: 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n bottom: 1px;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right .arrow, .bs-popover-auto[x-placement^=\"right\"] .arrow {\n left: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before,\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0.5rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before {\n left: 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n left: 1px;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom .arrow, .bs-popover-auto[x-placement^=\"bottom\"] .arrow {\n top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before,\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n border-width: 0 0.5rem 0.5rem 0.5rem;\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before {\n top: 0;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n top: 1px;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left .arrow, .bs-popover-auto[x-placement^=\"left\"] .arrow {\n right: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before,\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n border-width: 0.5rem 0 0.5rem 0.5rem;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before {\n right: 0;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n right: 1px;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n color: inherit;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: 0s 0.6s opacity;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: transparent no-repeat center center;\n background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-3by4::before {\n padding-top: 133.333333%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover {\n &:hover { @content; }\n}\n\n@mixin hover-focus {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { font-size: $h1-font-size; }\nh2, .h2 { font-size: $h2-font-size; }\nh3, .h3 { font-size: $h3-font-size; }\nh4, .h4 { font-size: $h4-font-size; }\nh5, .h5 { font-size: $h5-font-size; }\nh6, .h6 { font-size: $h6-font-size; }\n\n.lead {\n font-size: $lead-font-size;\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n font-size: $display1-size;\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n font-size: $display2-size;\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n font-size: $display3-size;\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n font-size: $display4-size;\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n font-size: $small-font-size;\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled;\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n font-size: $blockquote-font-size;\n}\n\n.blockquote-footer {\n display: block;\n font-size: $blockquote-small-font-size;\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all ``s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid;\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid;\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer / 2;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: $figure-caption-font-size;\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n// stylelint-disable indentation, media-query-list-comma-newline-after\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n}\n","// Single side border-radius\n\n@mixin border-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-radius: $radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n","// Inline code\ncode {\n font-size: $code-font-size;\n color: $code-color;\n word-break: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n font-size: $kbd-font-size;\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n font-size: $code-font-size;\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n}\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but with 100% width for\n// fluid, full width layouts.\n\n@if $enable-grid-classes {\n .container-fluid {\n @include make-container();\n }\n}\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col#{$infix}-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n margin-bottom: $spacer;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n\n .table {\n background-color: $body-bg;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: 2 * $table-border-width;\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover {\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(odd) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover {\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar; // See https://github.com/twbs/bootstrap/pull/10057\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background, $border: null) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n\n @if $border != null {\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $border;\n }\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n height: $input-height;\n padding: $input-padding-y $input-padding-x;\n font-size: $input-font-size;\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on `s in CSS.\n @if $enable-rounded {\n // Manually use the if/else instead of the mixin to account for iOS override\n border-radius: $input-border-radius;\n } @else {\n // Otherwise undo the iOS default\n border-radius: 0;\n }\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on ` receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: calc(#{$input-padding-y} + #{$input-border-width});\n padding-bottom: calc(#{$input-padding-y} + #{$input-border-width});\n margin-bottom: 0; // Override the `
+
+ + +
+ + +
  • +
    Publish
    +
    +
    + + +
    + + Version Up +
    + +
    + + Audio Only +
    + +
    +
    +
    + + +
    +
    +
    + +
    + + Path to sending data json +
    + +
    +
    +
    + + +
    +
    +
    + +
    + + Path to getting data json + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
  • +
  • +
  • + +
  • +
    Load/Update assets to timeline
    +
    +
    + + Type +
    + +
    + Ext +
    + +
    +
    + +
    +
    +
  • + + +
    + +
    + +
    +
    Output
    +
    + +
    + + + + + + \ No newline at end of file diff --git a/pype/premiere/static_ppro/js/avalon.js b/pype/premiere/static_ppro/js/avalon.js new file mode 100644 index 0000000000..6400c2f964 --- /dev/null +++ b/pype/premiere/static_ppro/js/avalon.js @@ -0,0 +1,367 @@ +/* global CSInterface, $, querySelector, api, displayResult */ +var csi = new CSInterface(); +var output = document.getElementById('output'); + +var rootFolderPath = csi.getSystemPath(SystemPath.EXTENSION); +var timecodes = cep_node.require('node-timecodes'); +var process = cep_node.require('process'); + + +function getEnv() { + csi.evalScript('pype.getProjectFileData();', function (result) { + process.env.EXTENSION_PATH = rootFolderPath + window.ENV = process.env; + var resultData = JSON.parse(result); + for (key in resultData) { + window.ENV[key] = resultData[key]; + }; + csi.evalScript('pype.setEnvs(' + JSON.stringify(window.ENV) + ')'); + }); +} + +function renderClips() { + csi.evalScript('pype.transcodeExternal(' + rootFolderPath + ');', function (result) { + displayResult(result); + }); +} + +function displayResult(r) { + console.log(r); + csi.evalScript('$.writeln( ' + JSON.stringify(r) + ' )'); + output.classList.remove("error"); + output.innerText = r; +} + +function displayError(e) { + output.classList.add("error"); + output.innerText = e.message; +} + +function loadJSX() { + // get the appName of the currently used app. For Premiere Pro it's "PPRO" + var appName = csi.hostEnvironment.appName; + var extensionPath = csi.getSystemPath(SystemPath.EXTENSION); + + // load general JSX script independent of appName + var extensionRootGeneral = extensionPath + '/jsx/'; + csi.evalScript('$._ext.evalFiles("' + extensionRootGeneral + '")'); + + // load JSX scripts based on appName + var extensionRootApp = extensionPath + '/jsx/' + appName + '/'; + csi.evalScript('$._ext.evalFiles("' + extensionRootApp + '")'); + // csi.evalScript('$._PPP_.logConsoleOutput()'); + getEnv(); + + csi.evalScript('$._PPP_.updateEventPanel( "' + "all plugins are loaded" + '" )'); + csi.evalScript('$._PPP_.updateEventPanel( "' + "testing function done" + '" )'); + +} + +// run all at loading +loadJSX() + + +function loadAnimationRendersToTimeline() { + // it will get type of asset and extension from input + // and start loading script from jsx + var $ = querySelector('#load'); + var data = {}; + data.subset = $('input[name=type]').value; + data.subsetExt = $('input[name=ext]').value; + var requestList = []; + // get all selected clips + csi.evalScript('pype.getClipsForLoadingSubsets( "' + data.subset + '" )', function (result) { + // TODO: need to check if the clips are already created and this is just updating to last versions + var resultObj = JSON.parse(result); + var instances = resultObj[0]; + var numTracks = resultObj[1]; + + var key = ''; + // creating requesting list of dictionaries + for (key in instances) { + var clipData = {}; + clipData.parentClip = instances[key]; + clipData.asset = key; + clipData.subset = data.subset; + clipData.representation = data.subsetExt; + requestList.push(clipData); + } + // gets data from mongodb + api.load_representations(window.ENV['AVALON_PROJECT'], requestList).then( + function (avalonData) { + // creates or updates data on timeline + var makeData = {}; + makeData.binHierarchy = data.subset + '/' + data.subsetExt; + makeData.clips = avalonData; + makeData.numTracks = numTracks; + csi.evalScript('pype.importFiles( ' + JSON.stringify(makeData) + ' )'); + } + ); + }); +} + +function evalScript(script) { + var callback = function (result) { + displayResult(result); + }; + csi.evalScript(script, callback); +} + +function deregister() { + api.deregister_plugin_path().then(displayResult); +} + +function register() { + var $ = querySelector('#register'); + var path = $('input[name=path]').value; + api.register_plugin_path(path).then(displayResult); +} + +function getStagingDir() { + // create stagingDir + const fs = require('fs-extra'); + const os = require('os'); + const path = require('path'); + const UUID = require('pure-uuid'); + const id = new UUID(4).format(); + const stagingDir = path.join(os.tmpdir(), id); + + fs.mkdirs(stagingDir); + return stagingDir; + +} + +function convertPathString(path) { + return path.replace( + new RegExp('\\\\', 'g'), '/').replace(new RegExp('//\\?/', 'g'), ''); +} + +function publish() { + var $ = querySelector('#publish'); + // var gui = $('input[name=gui]').checked; + var gui = true; + var versionUp = $('input[name=version-up]').checked; + var audioOnly = $('input[name=audio-only]').checked; + var jsonSendPath = $('input[name=send-path]').value; + var jsonGetPath = $('input[name=get-path]').value; + var publish_path = window.ENV['PUBLISH_PATH']; + + if (jsonSendPath == '') { + // create temp staging directory on local + var stagingDir = convertPathString(getStagingDir()); + + // copy project file to stagingDir + const fs = require('fs-extra'); + const path = require('path'); + + csi.evalScript('pype.getProjectFileData();', function (result) { + displayResult(result); + var data = JSON.parse(result); + displayResult(stagingDir); + displayResult(data.projectfile); + var destination = convertPathString(path.join(stagingDir, data.projectfile)); + displayResult('copy project file'); + displayResult(data.projectfile); + displayResult(destination); + fs.copyFile(data.projectpath, destination); + displayResult('project file coppied!'); + }); + + // publishing file + csi.evalScript('pype.getPyblishRequest("' + stagingDir + '", ' + audioOnly + ');', function (r) { + var request = JSON.parse(r); + displayResult(JSON.stringify(request)); + + csi.evalScript('pype.encodeRepresentation(' + JSON.stringify(request) + ');', function (result) { + // create json for pyblish + var jsonfile = require('jsonfile'); + var jsonSendPath = stagingDir + '_send.json' + var jsonGetPath = stagingDir + '_get.json' + $('input[name=send-path]').value = jsonSendPath; + $('input[name=get-path]').value = jsonGetPath; + var jsonContent = JSON.parse(result); + jsonfile.writeFile(jsonSendPath, jsonContent); + var checkingFile = function (path) { + var timeout = 1000; + setTimeout(function () { + if (fs.existsSync(path)) { + // register publish path + api.register_plugin_path(publish_path).then(displayResult); + // send json to pyblish + api.publish(jsonSendPath, jsonGetPath, gui).then(function (result) { + // check if resulted path exists as file + if (fs.existsSync(result.get_json_path)) { + // read json data from resulted path + displayResult('Updating metadata of clips after publishing'); + + jsonfile.readFile(result.get_json_path, function (err, json) { + csi.evalScript('pype.dumpPublishedInstancesToMetadata(' + JSON.stringify(json) + ');'); + }) + + // version up project + if (versionUp) { + displayResult('Saving new version of the project file'); + csi.evalScript('pype.versionUpWorkFile();'); + }; + } else { + // if resulted path file not existing + displayResult('Publish has not been finished correctly. Hit Publish again to publish from already rendered data, or Reset to render all again.'); + }; + + }); + + } else { + displayResult('waiting'); + checkingFile(path); + }; + }, + timeout) + }; + + checkingFile(jsonContent.waitingFor) + }); + }); + } else { + // register publish path + api.register_plugin_path(publish_path).then(displayResult); + // send json to pyblish + api.publish(jsonSendPath, jsonGetPath, gui).then(function (result) { + // check if resulted path exists as file + if (fs.existsSync(result.get_json_path)) { + // read json data from resulted path + displayResult('Updating metadata of clips after publishing'); + + jsonfile.readFile(result.get_json_path, function (err, json) { + csi.evalScript('pype.dumpPublishedInstancesToMetadata(' + JSON.stringify(json) + ');'); + }) + + // version up project + if (versionUp) { + displayResult('Saving new version of the project file'); + csi.evalScript('pype.versionUpWorkFile();'); + }; + } else { + // if resulted path file not existing + displayResult('Publish has not been finished correctly. Hit Publish again to publish from already rendered data, or Reset to render all again.'); + }; + + }); + }; + // $('input[name=send-path]').value = ''; + // $('input[name=get-path]').value = ''; +} + +function context() { + var $ = querySelector('#context'); + var project = $('input[name=project]').value; + var asset = $('input[name=asset]').value; + var task = $('input[name=task]').value; + var app = $('input[name=app]').value; + api.context(project, asset, task, app).then(displayResult); +} + +function tc(timecode) { + var seconds = timecodes.toSeconds(timecode); + var timec = timecodes.fromSeconds(seconds); + displayResult(seconds); + displayResult(timec); +} + +function rename() { + var $ = querySelector('#rename'); + var data = {}; + data.ep = $('input[name=episode]').value; + data.epSuffix = $('input[name=ep_suffix]').value; + + if (!data.ep) { + csi.evalScript('pype.alert_message("' + 'Need to fill episode code' + '")'); + return; + }; + + if (!data.epSuffix) { + csi.evalScript('pype.alert_message("' + 'Need to fill episode longer suffix' + '")'); + return; + }; + + csi.evalScript('br.renameTargetedTextLayer( ' + JSON.stringify(data) + ' );', function (result) { + displayResult(result); + }); +} + +// bind buttons +$('#btn-getRernderAnimation').click(function () { + loadAnimationRendersToTimeline(); +}); + +$('#btn-rename').click(function () { + rename(); +}); + +$('#btn-set-context').click(function () { + context(); +}); + +$('#btn-register').click(function () { + register(); +}); + +$('#btn-deregister').click(function () { + deregister(); +}); + +$('#btn-publish').click(function () { + publish(); +}); + +$('#btn-send-reset').click(function () { + var $ = querySelector('#publish'); + $('input[name=send-path]').value = ''; +}); +$('#btn-get-reset').click(function () { + var $ = querySelector('#publish'); + $('input[name=get-path]').value = ''; +}); +$('#btn-get-active-sequence').click(function () { + evalScript('pype.getActiveSequence();'); +}); + +$('#btn-get-selected').click(function () { + $('#output').html('getting selected clips info ...'); + evalScript('pype.getSelectedItems();'); +}); + +$('#btn-get-env').click(function () { + displayResult(window.ENV); +}); + +$('#btn-get-projectitems').click(function () { + evalScript('pype.getProjectItems();'); +}); + +$('#btn-metadata').click(function () { + var $ = querySelector('#publish'); + var path = $('input[name=get-path]').value; + var jsonfile = require('jsonfile'); + displayResult(path); + jsonfile.readFile(path, function (err, json) { + csi.evalScript('pype.dumpPublishedInstancesToMetadata(' + JSON.stringify(json) + ');'); + displayResult('Metadata of clips after publishing were updated'); + }) + + +}); +$('#btn-get-frame').click(function () { + evalScript('$._PPP_.exportCurrentFrameAsPNG();'); +}); + +$('#btn-tc').click(function () { + tc('00:23:47:10'); +}); + +$('#btn-generateRequest').click(function () { + evalScript('pype.getPyblishRequest();'); +}); + +$('#btn-newWorkfileVersion').click(function () { + evalScript('pype.versionUpWorkFile();'); +}); diff --git a/pype/premiere/static_ppro/js/build.js b/pype/premiere/static_ppro/js/build.js new file mode 100644 index 0000000000..ea3f18bf9e --- /dev/null +++ b/pype/premiere/static_ppro/js/build.js @@ -0,0 +1,4862 @@ +var app = angular.module("Plugin", ["ui-rangeSlider", "ui.bootstrap"]); +app.run(["$rootScope", "MainHelper", function($rootScope, MainHelper) { + MainHelper.init(BM_VIDEO, 15) +}]), app.controller("ModalIntroController", function($scope, $uibModal, CreateOnFileSystemService, DestinationsService) { + $scope.items = [], $scope.obj = { + state: 1 + }, $scope.$root.$on("intro requested", function(event) { + console.log("ModalIntroController event handler"), $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_INTRO_HTML, + backdrop: "static", + controller: ModalIntroInstanceCtrl, + windowClass: "modal-intro" + }).result.then(function() { + console.log("ModalIntroController OK"), CreateOnFileSystemService.createDestinationBaseFolder(), DestinationsService.saveItem() + }, function() { + console.log("ModalIntroController CANCELED") + }) + } +}); + +var ModalIntroInstanceCtrl = function($scope, $uibModalInstance, BrowseDestinationService, AppModel) { + $scope.obj = { + state: 1, + title: "", + message: "", + labelLeft: [!1, "PREVIOUS"], + labelCenter: [!1, ""], + labelRight: [!0, "NEXT"], + stateImage: [!0, ""], + selectedFolder: AppModel.currentBaseFolder + }, $scope.onChange = function() { + switch (1 < $scope.obj.state && ($scope.obj.stateImage = [!0, STATE_IMG + $scope.obj.state + ".png"]), $scope.obj.state) { + case 1: + $scope.obj.stateName = "", $scope.obj.stateImage = [!1, ""], $scope.obj.labelLeft = [!1, "PREVIOUS"], $scope.obj.title = "Welcome!", $scope.obj.message = "Thanks for downloading the Pond5 Adobe Add-On.
    Click through this short tutorial to learn some of the basics."; + break; + case 2: + $scope.obj.labelLeft = [!0, "PREVIOUS"], $scope.obj.stateName = "search", $scope.obj.title = "", $scope.obj.message = "Start by searching our massive library of royalty-free video clips
    and easily add them to your working projects."; + break; + case 3: + $scope.obj.stateName = "filters", $scope.obj.labelLeft = [!0, "PREVIOUS"], $scope.obj.message = "Use the toolbar on the left to filter your search results,
    view your previews, and update your directory folder."; + break; + case 4: + $scope.obj.stateName = "collections", $scope.obj.message = "View and create new collections below.
    We've even added 50 free clips to get you started!"; + break; + case 5: + $scope.obj.stateName = "login", $scope.obj.labelCenter = [!1, "SELECT"], $scope.obj.labelRight = [!0, "NEXT"], $scope.obj.message = "Log in to your Pond5 account here for easy checkout
    once you've found the perfect clips for your project."; + break; + case 6: + $scope.obj.stateName = "", $scope.obj.labelLeft = [!0, "PREVIOUS"], $scope.obj.labelCenter = [!0, "SELECT"], $scope.obj.labelRight = [!0, "FINISH"], $scope.obj.message = "Select your destination folder to get started. Pond5 media will be saved in this folder.", 0 < AppModel.currentBaseFolder.length && ($scope.obj.message = "Select your destination folder to get started.
    The default folder is " + AppModel.currentBaseFolder) + } + }, $scope.buttonLeftClicked = function() { + $scope.obj.state--, $scope.onChange(), getStateObject($scope.obj.stateName) + }, $scope.buttonCenterClicked = function() { + $scope.obj.selectedFolder = BrowseDestinationService.browse(), $scope.obj.message = "Your current destination folder is:
    " + $scope.obj.selectedFolder + }, $scope.buttonRightClicked = function() { + console.log("ModalIntroController buttonRightClicked"), $scope.obj.state < 6 ? ($scope.obj.state++, $scope.onChange(), getStateObject($scope.obj.stateName)) : (console.log("ModalIntroController buttonRightClicked", $scope.obj.selectedFolder), BrowseDestinationService.save($scope.obj.selectedFolder), $uibModalInstance.close()) + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + }, getStateObject = function(stateName) { + console.log("modalIntroController look for: ", stateName), INTRO_DATA.forEach(function(entry) { + var obj = {}; + entry.stateName === stateName ? (console.log("modalIntroController found stateName: ", entry), obj.stateName = entry.stateName, obj.arrowClass = entry.arrowClass, obj.posX = entry.posX, obj.posY = entry.posY, console.log("modalIntroController found obj: ", obj)) : (obj.stateName = stateName, obj.arrowClass = ""), $scope.$root.$emit("intro asset requested", obj) + }) + }, $scope.onChange() +}; +PLUGIN_VERSION = "", HOST_NAME = "PPRO", THIRD_PARTY = "", MEDIA_TYPES = ["Footage", "Music", "SFX"], BUTTON_REPLACE_LABEL = "REPLACE WITH HI-RES CLIPS", BUTTON_REPLACE_TOOLTIP = "Replace lo-res with paid items", MODAL_REPLACE_HEADER = "Replace With Hi-Res Clips", MODAL_REPLACE_CONTENT = "The selected items below will be replaced by full resolution versions after you complete checkout. Items already in your account history will also be downloaded.", MODAL_REPLACE_RES_TITLE = "RESOLUTION", MODAL_INTRO_SEARCH = "Start by searching our massive library of royalty-free video clips
    and easily add them to your working projects.", MODAL_INTRO_COLLECTIONS = "View and create new collections below.
    We've even added 50 free clips to get you started!", MODAL_INTRO_LOGIN = "Log in to your Pond5 account here for easy checkout
    once you've found the perfect clips for your project.", INTRO_DATA = [{ + state: 7, + stateName: "downloads", + arrowClass: ".intro-asset-arrow-left", + posY: ["top", "96px"], + posX: ["left", "60px"] +}, { + state: 3, + stateName: "filters", + arrowClass: ".intro-asset-arrow-left", + posY: ["top", "60px"], + posX: ["left", "55px"] +}, { + state: 9, + stateName: "destination", + arrowClass: ".intro-asset-arrow-left", + posY: ["bottom", "55px"], + posX: ["left", "60px"] +}, { + state: 4, + stateName: "collections", + arrowClass: ".intro-asset-arrow-down", + posY: ["bottom", "140px"], + posX: ["left", "260px"] +}, { + state: 2, + stateName: "search", + arrowClass: ".intro-asset-arrow-up", + posY: ["top", "60px"], + posX: ["left", "165px"] +}, { + state: 5, + stateName: "login", + arrowClass: ".intro-asset-arrow-up", + posY: ["top", "60px"], + posX: ["right", "75px"] +}], app.service("ReplaceService", ["$rootScope", "ReplaceModel", "Service", "ReplaceServiceShared", function($rootScope, ReplaceModel, Service, ReplaceServiceShared) { + var call = { + onClipFSCollected: function() { + call.getSequences() + }, + getSequences: function() { + csInterface.evalScript("getSequences()", function(result) { + var sequences = JSON.parse(result).sequences; + console.log("\nReplaceService sequences NEW", sequences.length, sequences), ReplaceModel.setSequences(sequences) + }) + }, + getMedia: function() { + var obj = ReplaceModel.sequences; + csInterface.evalScript("getSequenceItems(" + JSON.stringify(obj) + ")", function(result) { + var clipsInSequences = JSON.parse(result).data; + ReplaceModel.clipsInSequences = clipsInSequences, console.log("\nReplaceService clipsInSequences", ReplaceModel.clipsInSequences), csInterface.evalScript("getProjectItems()", function(result) { + call.getMissingItemIDs() + }) + }) + }, + getClipsInSelectedSequences: function() { + for (var clipsInSequences = ReplaceModel.clipsInSequences, clipsInSelectedSequences = [], s = 0; s < ReplaceModel.sequences.length; s++) + for (var j = 0; j < clipsInSequences.length; j++) + if (ReplaceModel.sequences[s].sequenceID === clipsInSequences[j].sequenceID && ReplaceModel.sequences[s].checked) + for (var k = 0; k < clipsInSequences[j].clipNames.length; k++) clipsInSelectedSequences.push(clipsInSequences[j].clipNames[k]); + return clipsInSelectedSequences + }, + getMissingItemIDs: function() { + var clipsInSelectedSequences = call.getClipsInSelectedSequences(); + clipsInSelectedSequences = ReplaceServiceShared.removeDuplicates(clipsInSelectedSequences), console.log("\nReplaceService clipsInSelectedSequences after removing duplicates: ", clipsInSelectedSequences); + var previewNamesonFS = ReplaceServiceShared.getPreviewsOnFSNames(); + clipsInSelectedSequences = ReplaceServiceShared.filterNonP5Clips(clipsInSelectedSequences, previewNamesonFS), console.log("\nReplaceService after filterNonP5Clips", clipsInSelectedSequences); + var previewIDs = ReplaceServiceShared.getPreviewsIDs(clipsInSelectedSequences); + console.log("\nReplaceService previewIDs: " + previewIDs), ReplaceServiceShared.setReplaceProp(previewIDs), console.log("\nReplaceService after set replace: " + ReplaceModel.hiresOnFS); + var hiresIDs = ReplaceServiceShared.getHiresIDsonFS(); + console.log("\nReplaceService hiresIDs: " + hiresIDs); + var missingItemIDs = _(previewIDs).difference(hiresIDs), + missingIDsToString = missingItemIDs.join(","); + 0 < missingItemIDs.length ? Service.getMissingItems(missingIDsToString) : 0 < hiresIDs.length ? call.onPurchasedAndDownloaded() : 0 === clipsInSelectedSequences.length && (ReplaceModel.setState(DEFAULT), $rootScope.$emit("modal simple requested", ["", "There are are currently no Pond5 previews in the sequence(s) you've selected."])) + }, + onPurchasedAndDownloaded: function() { + var hasReplaceCandidates = !1; + if (ReplaceModel.hiresOnFS.forEach(function(entry) { + entry.replace && (hasReplaceCandidates = !0) + }), !hasReplaceCandidates) return $rootScope.$emit("modal simple requested", ["", "Replacing previews by hi-res clips has been canceled"]), void ReplaceModel.setState(DEFAULT); + var obj = { + hiresOnFS: ReplaceModel.hiresOnFS + }; + csInterface.evalScript("replaceClips(" + JSON.stringify(obj) + ")", function(result) { + $rootScope.$emit("modal simple requested", ["", "Your previews have been successfully replaced by your purchased clips. Right-click the clips and choose Scale to Frame Size to scale them correctly."]), ReplaceModel.setState(DEFAULT) + }) + } + }; + return call +}]), app.controller("ModalAddDestinationController", function($scope, $uibModal, UserModel, AppModel, CreateOnFileSystemService, DestinationsService) { + $scope.obj = {}, $scope.$root.$on("modal add destination requested", function() { + console.log("ModalAddDestinationController event handler", UserModel.getFirstTimeUser()), $scope.obj.title = "Add a destination folder", $scope.obj.content = "Please select a new folder to store your previews and purchased items.", $scope.obj.okButtonLabel = "APPLY", $scope.obj.selectedFolderPrefix = "Current folder: ", $scope.obj.selectedFolder = AppModel.currentBaseFolder, $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_ADD_DESTINATION_HTML, + controller: ModalAddDestinatonInstanceCtrl, + size: size, + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + console.log("ModalAddDestinationController OK", AppModel.currentBaseFolder), $scope.onClicked() + }, function() { + console.log("ModalAddDestinationController CANCEL", AppModel.currentBaseFolder), $scope.onClicked() + }) + }, $scope.onClicked = function() { + console.log("ModalAddDestinationController onClicked"), UserModel.getFirstTimeUser() && $scope.$root.$emit("modal freebies"), CreateOnFileSystemService.createDestinationBaseFolder(), DestinationsService.saveItem() + } +}); +var ModalAddDestinatonInstanceCtrl = function($scope, $uibModalInstance, obj, BrowseDestinationService) { + $scope.obj = {}, $scope.obj.showTitle = obj.showTitle, $scope.obj.title = obj.title, $scope.obj.content = obj.content, $scope.obj.selectedFolder = obj.selectedFolder, $scope.obj.selectedFolderPrefix = obj.selectedFolderPrefix, $scope.obj.okButtonLabel = obj.okButtonLabel, $scope.browse = function() { + console.log("ModalAddDestinatonInstanceCtrl browse"), $scope.obj.selectedFolder = BrowseDestinationService.browse() + }, $scope.ok = function() { + BrowseDestinationService.save($scope.obj.selectedFolder), $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalSelectSequencesController", function($scope, $uibModal, ReplaceModel, ReplaceService) { + $scope.items = [], $scope.$root.$on("modal select sequences", function(event, data) { + $scope.items = data, $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_SELECT_SEQUENCES_HTML, + controller: ModalSelectSequencesInstanceCtrl, + size: size, + resolve: { + items: function() { + return $scope.items + } + } + }).result.then(function() { + console.log("ModalSelectSequencesController OK: ", $scope.items); + for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected && (ReplaceModel.sequences[i].checked = !0); + ReplaceService.getMedia() + }, function() { + ReplaceModel.setState(DEFAULT) + }) + } +}); +var ModalSelectSequencesInstanceCtrl = function($scope, $uibModalInstance, items) { + $scope.items = items, $scope.obj = { + showWarning: !1 + }, $scope.ok = function() { + for (var checked = !1, i = 0; i < $scope.items.length; i++) $scope.items[i].selected && (checked = !0); + checked ? $uibModalInstance.close() : $scope.obj.showWarning = !0 + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.factory("MainHelper", ["$rootScope", "AppModel", "StartUpService", "SearchModel", function($rootScope, AppModel, StartUpService, SearchModel) { + var result = { + init: function(mediaType, sumOfBitmasks) { + csInterface = new CSInterface, csInterface.addEventListener("LogEvent", function(evt) { + console.log("JSX : " + evt.data) + }); + var rootFolderPath = csInterface.getSystemPath(SystemPath.EXTENSION); + AppModel.rootFolderPath = rootFolderPath, fs = require("fs"), os = require("os"), path = require("path"), url = require("url"), https = require("https"), xml2js = require(rootFolderPath + "/node_modules/xml2js/lib/xml2js.js"), walk = require(rootFolderPath + "/node_modules/walk/lib/walk.js"), junk = require(rootFolderPath + "/node_modules/junk/index.js"), rimraf = require(rootFolderPath + "/node_modules/rimraf/rimraf.js"), opn = require(rootFolderPath + "/node_modules/opn/index.js"), DecompressZip = require(rootFolderPath + "/node_modules/decompress-zip/lib/decompress-zip.js"), $("#logo").click(function() { + location.reload() + }), result.readManifestXML(), SearchModel.sumOfBitmasks = sumOfBitmasks, $rootScope.$emit("media filter change", mediaType), setTimeout(function() { + AppModel.setEnv() + }, 2e3) + }, + readManifestXML: function() { + var file = AppModel.rootFolderPath + "/CSXS/manifest.xml"; + fs.readFile(file, "utf8", function(err, data) { + if (err) throw err; + result.parseXML(data) + }) + }, + parseXML: function(xml) { + var parser = new xml2js.Parser; + parser.addListener("end", function(res) { + PLUGIN_VERSION = res.ExtensionManifest.$.ExtensionBundleVersion, console.log("mainHelper parsed manifest xml, version:", PLUGIN_VERSION), result.loadJSX() + }), parser.parseString(xml) + }, + loadJSX: function(fileName) { + var jsxPath = AppModel.rootFolderPath + "./js/vendor/json2.js"; + console.log("mainHelper loadJSX:", jsxPath), csInterface.evalScript('$.evalFile("' + jsxPath + '")', function(result) {}) + } + }; + return result +}]), app.service("BrowseDestinationService", ["AppModel", function(AppModel) { + this.browse = function() { + var result = window.cep.fs.showOpenDialog(!1, !0, "Select a folder for your previews and hi-res downloads.", ""), + selectedFolder = AppModel.currentBaseFolder; + return console.log("BrowseDestinationService folder chosen, result.err: ", result.err), 0 == result.err ? (console.log("BrowseDestinationService folder chosen: ", result.data[0]), result.data[0] && (selectedFolder = result.data[0])) : selectedFolder = "This folder cannot be selected. Please choose another folder.", console.log("BrowseDestinationService return folder: ", selectedFolder), selectedFolder + }, this.save = function(selectedFolder) { + console.log("BrowseDestinationService save", AppModel.getOS(), "win" === AppModel.getOS()), "win" === AppModel.getOS() ? AppModel.currentBaseFolder = selectedFolder.replace(/\//g, "\\") : AppModel.currentBaseFolder = selectedFolder + } +}]), app.service("CreateFileCompleteService", ["ImportedPreviewsService", "DestinationsService", "UserService", function(ImportedPreviewsService, DestinationsService, UserService) { + return { + onFileReady: function(file) { + -1 != file.indexOf("imported_previews.xml") && ImportedPreviewsService.readXML(), -1 != file.indexOf("destinations.xml") && DestinationsService.readXML(), -1 != file.indexOf("user.xml") && UserService.readXML() + } + } +}]), app.factory("DestinationsService", ["$rootScope", "AppModel", "UserModel", function($rootScope, AppModel, UserModel) { + var result = { + xmlVersion: "", + readXML: function() { + result.file = AppModel.getDestinationsXML(), console.log("DestinationsService file: ", result.file), fs.readFile(result.file, "utf8", function(err, data) { + if (err) throw err; + result.xml = data, console.log("DestinationsService, xml:", result.xml), result.parseXML() + }) + }, + saveItem: function() { + var node = ''; + result.xml = result.xml.insert(result.xml.indexOf("destinations") + 13, node), result.writeToDisk() + }, + deleteItem: function() {}, + parseXML: function() { + var parser = new xml2js.Parser; + parser.addListener("end", function(res) { + var i; + result.parsedXML = res, AppModel.baseFolders = [], UserModel.setFirstTimeUser(!1), res.root.$[HOST_NAME] ? result.xmlVersion = res.root.$[HOST_NAME] : res.root.$.version ? result.xmlVersion = res.root.$.version : res.root.$.PPRO && (result.xmlVersion = res.root.$.PPRO), UserModel.setUID(res.root.$.id), PLUGIN_VERSION != result.xmlVersion && (console.log("DestinationsService other or no version number in xml, first time user: ", result.xmlVersion), UserModel.setFirstTimeUser(!0)); + var destinations = res.root.destinations[0].destination; + if (console.log("DestinationsService destinations: ", destinations), destinations) { + for (i = 0; i < destinations.length; i++) - 1 == AppModel.baseFolders.indexOf(destinations[i].$.destination) && fs.existsSync(destinations[i].$.destination + path.sep + "pond5") && AppModel.baseFolders.push(destinations[i].$.destination); + fs.stat(AppModel.baseFolders[0] + path.sep + "pond5", function(err, stats) { + err ? setTimeout(function() { + $rootScope.$emit("modal add destination requested") + }, 3e3) : AppModel.currentBaseFolder = AppModel.baseFolders[0] + }), console.log("DestinationsService AppModel.baseFolders : ", AppModel.baseFolders), console.log("DestinationsService currentBaseFolder : ", AppModel.currentBaseFolder) + } + if (UserModel.getFirstTimeUser()) { + var newVersion = HOST_NAME + '="' + PLUGIN_VERSION + '"'; + result.parsedXML.root.$[HOST_NAME] ? result.xml = result.xml.replace(HOST_NAME + '="' + result.xmlVersion + '"', newVersion) : result.parsedXML.root.$.version && "PPRO" === HOST_NAME ? result.xml = result.xml.replace('version="' + result.xmlVersion + '"', newVersion) : result.parsedXML.root.$.version && "PPRO" != HOST_NAME ? result.xml = result.xml.replace('version="' + result.xmlVersion + '"', 'version="' + result.xmlVersion + '" ' + newVersion) : result.parsedXML.root.$.PPRO && !result.parsedXML.root.$[HOST_NAME] && (result.xml = result.xml.replace('PPRO="' + result.xmlVersion + '"', 'PPRO="' + result.xmlVersion + '" ' + newVersion)), console.log("DestinationsService result.xml replaced: ", result.xml), console.log("DestinationsService getFirstTimeUser is true, show intro"), setTimeout(function() { + $rootScope.$emit("intro requested") + }, 3e3) + } + }), parser.parseString(result.xml) + }, + writeToDisk: function() { + fs.writeFile(result.file, result.xml, function(err) { + if (err) throw err; + result.readXML() + }) + } + }; + return result +}]), app.service("ImportService", ["$rootScope", function($rootScope) { + this.importClips = function(items) { + var i, importPaths = []; + for (i = 0; i < items.length; i++) console.log("ImportService item.canceled:", items[i].canceled), items[i].canceled || items[i].imported || (items[i].imported = !0, importPaths.push(items[i].downloadDestination + items[i].fileName)); + console.log("ImportService importPath:", importPaths); + var obj = { + paths: importPaths + }; + csInterface.evalScript("importClips(" + JSON.stringify(obj) + ")", function(result) { + console.log("ImportService result: ", result), $rootScope.$emit("on importing bin complete") + }) + } +}]), app.service("OpenURLService", [function() { + this.openURL = function(url) { + csInterface.openURLInDefaultBrowser(url) + } +}]), app.controller("AdvancedSearchController", function($scope, ViewStateModel, SearchModel, ViewStateService) { + $scope.obj = { + show: !1, + fpsItems: [{ + fps: "23.98" + }, { + fps: "24" + }, { + fps: "25" + }, { + fps: "29.97" + }, { + fps: "30" + }, { + fps: "60" + }, { + fps: "60+" + }], + resItems: [{ + res: "4K+", + param: "8K" + }, { + res: "4K", + param: "4K" + }, { + res: "2K", + param: "2K" + }, { + res: "HD (1080)", + param: "HD1080" + }, { + res: "HD (720)", + param: "HD720" + }, { + res: "SD", + param: "SD" + }, { + res: "Web", + param: "WEB" + }], + showCbFilters: !0, + _minPrice: 0, + _maxPrice: 500, + minPrice: function(newValue) { + return arguments.length ? $scope.obj._minPrice = newValue : $scope.obj._minPrice + }, + maxPrice: function(newValue) { + return 500 == $scope.obj._maxPrice ? $scope.obj.maxPriceValue = "$500+" : $scope.obj.maxPriceValue = "$" + $scope.obj._maxPrice, arguments.length ? $scope.obj._maxPrice = newValue : $scope.obj._maxPrice + }, + _minTime: 0, + _maxTime: 120, + minTime: function(newValue) { + return arguments.length ? $scope.obj._minTime = newValue : $scope.obj._minTime + }, + maxTime: function(newValue) { + return 120 == $scope.obj._maxTime ? $scope.obj.showTimePlusSign = !0 : $scope.obj.showTimePlusSign = !1, arguments.length ? $scope.obj._maxTime = newValue : $scope.obj._maxTime + } + }, $scope.oneAtATime = !0, $scope.reset = function() { + for ($scope.obj._minPrice = 0, $scope.obj._maxPrice = 500, $scope.obj._minTime = 0, $scope.obj._maxTime = 120, SearchModel.fps = "", SearchModel.fpsgt = "", SearchModel.res = "", SearchModel.pricegt = "", SearchModel.pricelt = "", SearchModel.durationgt = "", SearchModel.durationlt = "", i = 0; i < $scope.obj.fpsItems.length; i++) $scope.obj.fpsItems[i].checked = !1; + for (i = 0; i < $scope.obj.resItems.length; i++) $scope.obj.resItems[i].checked = !1 + }, $scope.reset(), $scope.$root.$on("filters button clicked", function(event, state) { + $scope.obj.show = state + }), $scope.$root.$on("media filter change", function(event, data) { + data == BM_VIDEO || data == BM_PUBLIC_DOMAIN ? $scope.obj.showCbFilters = !0 : ($scope.obj.showCbFilters = !1, $scope.reset()), data == BM_AFTER_EFFECTS ? $scope.obj.showDuration = !1 : $scope.obj.showDuration = !0 + }), $scope.change = function() { + var fpsgt, fps = " fps", + res = " resolutions"; + for (i = 0; i < $scope.obj.fpsItems.length - 1; i++) $scope.obj.fpsItems[i].checked && (fps += ":" + $scope.obj.fpsItems[i].fps); + for (fpsgt = $scope.obj.fpsItems[6].checked ? " fpsgt:60" : "", i = 0; i < $scope.obj.resItems.length; i++) $scope.obj.resItems[i].checked && (res += ":" + $scope.obj.resItems[i].param); + fps.length <= 5 ? fps = "" : fpsgt = "", res.length <= 13 && (res = ""), SearchModel.fps = fps, SearchModel.fpsgt = fpsgt, SearchModel.res = res, SearchModel.resultType = "replace", SearchModel.page = 0, ViewStateService.viewRequested("search") + }, $scope.onHideFiltersClicked = function() { + $scope.obj.show = !1, $scope.$root.$emit("filters button clicked", !1) + }, $scope.onResetFiltersClicked = function() { + $scope.reset(), $scope.change() + }, $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, function() { + "cart" !== ViewStateModel.getState() && "downloads" !== ViewStateModel.getState() || ($scope.obj.show = !1) + }, !0), window.addEventListener("rangeSliderOff", function(e) { + "" == $scope.obj._minPrice ? SearchModel.pricegt = "" : SearchModel.pricegt = " pricegt:" + $scope.obj._minPrice, "500" == $scope.obj._maxPrice ? SearchModel.pricelt = "" : SearchModel.pricelt = " pricelt:" + $scope.obj._maxPrice, "" == $scope.obj._minTime ? SearchModel.durationgt = "" : SearchModel.durationgt = " durationgt:" + $scope.obj._minTime, "120" == $scope.obj._maxTime ? SearchModel.durationlt = "" : SearchModel.durationlt = " durationlt:" + $scope.obj._maxTime, $scope.change() + }, !1) +}), app.controller("AlertController", function($scope) { + $scope.alerts = [], $scope.addAlert = function() { + console.log("AlertController add"), $scope.alerts.push({ + msg: "Another alert!" + }) + }, $scope.closeAlert = function(index) { + $scope.alerts.splice(index, 1) + } +}), app.controller("BinsController", function($scope, BinsModel, Service, LoginModel, ViewStateModel, ViewStateService) { + $scope.obj = {}, $scope.obj.showImportAll = !1, $scope.obj.showSelect = !1, $scope.obj.direction = "dropup", $scope.loginModel = function() { + return LoginModel.loggedIn + }, $scope.viewStateModel = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.loginModel, function() { + LoginModel.loggedIn ? $scope.obj.showSelect = !0 : $scope.obj.showSelect = !1 + }), $scope.$watch($scope.viewStateModel, function() { + "bins" != ViewStateModel.getState() && ($scope.obj.selectedNameFormatted = "Collection") + }), $scope.$root.$on("onBins", function(event) { + $scope.bins = BinsModel.bins + }), $scope.onClick = function() { + console.log("BinsController onClick"), $scope.$root.$emit("select clicked") + }, $scope.onChange = function(bin) { + console.log("onChange, bin: ", bin), 14 < bin.name.length ? $scope.obj.selectedNameFormatted = bin.name.substr(0, 14) + "..." : $scope.obj.selectedNameFormatted = bin.name, $scope.obj.open = !1, $scope.selected = bin, $scope.selected && (BinsModel.selectedBin = bin, $scope.$root.$emit("bin selected", bin.name), ViewStateService.viewRequested("bins")) + }, $scope.onDelete = function(bin) { + console.log("onDelete, bin: ", bin) + }, $scope.toggled = function(open) { + $scope.obj.direction = open ? "down" : "dropup" + }, $scope.onAddClicked = function() { + console.log("onAddClicked"), $scope.$root.$emit("modal add collection requested") + }, $scope.onRemoveClicked = function() { + console.log("onRemoveClicked"), $scope.$root.$emit("modal remove collection requested") + } +}), app.controller("CartController", function($scope, Service, ViewStateService, CartModel, LoginModel, AnalyticsService) { + $scope.obj = { + numberOfItem: 0, + clearCartIcon: CLEAR_CART_TRASH_IMG, + imageUrl: CART_BUTTON_IMG, + cartButtonStyle: "button-cart-logged-out" + }, $scope.cartModel = function() { + return CartModel.cartVO + }, $scope.$watch($scope.cartModel, function() { + CartModel.cartVO.items && ($scope.obj.numberOfItems = CartModel.cartVO.items.length) + }), $scope.loginModel = function() { + return LoginModel + }, $scope.$watch($scope.loginModel, function() { + LoginModel.getLoggedIn() ? $scope.obj.cartButtonStyle = "button-cart-logged-in" : ($scope.obj.cartButtonStyle = "button-cart-logged-out", $scope.obj.numberOfItems = "") + }, !0), $scope.onCartButtonClicked = function() { + ViewStateService.viewRequested("cart"); + var ga = { + ec: "cart" + }; + AnalyticsService.sendData(ga) + } +}), app.controller("CheckOutController", function($scope, Service, ViewStateModel, CheckOutService, CartModel) { + $scope.obj = { + show: !1, + disabled: !0, + info: "", + showInfo: !1, + subTotalText: "", + showVAT: !1, + lineStyle: "", + totalStyle: "", + remainingStyle: "", + cartInfoStyle: "" + }, $scope.CartModel = function() { + return CartModel.cartVO + }, $scope.$watch($scope.CartModel, function() { + CartModel.cartVO.items && 0 < CartModel.cartVO.items.length ? $scope.obj.disabled = !1 : $scope.obj.disabled = !0 + }, !0), $scope.$root.$on("checkout complete", function() { + $scope.obj.disabled = !1 + }), $scope.$root.$on("billing info canceled", function() { + $scope.obj.disabled = !1 + }), $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, function() { + "cart" === ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 + }, !0), $scope.onClick = function() { + $scope.obj.disabled = !0, $scope.$root.$emit("on modal choose billing info requested"), $scope.onOut() + }, $scope.onOver = function() { + $scope.obj.showInfo = !0, $scope.showData() + }, $scope.onOut = function() { + $scope.obj.showInfo = !1 + }, $scope.showData = function() { + var data = CartModel.getCartTotal(); + data && ($scope.obj.subTotalText = data.subtotals.beforeDiscounts, data.vatData.display ? $scope.obj.showVAT = !0 : $scope.obj.showVAT = !1, $scope.obj.showVAT ? ($scope.obj.cartInfoStyle = "cart-info-vat", $scope.obj.lineStyle = "cart-info-line-vat", $scope.obj.totalStyle = "cart-info-total-vat", $scope.obj.remainingStyle = "cart-info-remaining-vat", $scope.obj.vatPerc = data.vatData.percentage, $scope.obj.vat = data.vatData.amount) : ($scope.obj.cartInfoStyle = "cart-info-no-vat", $scope.obj.lineStyle = "cart-info-line-no-vat", $scope.obj.totalStyle = "cart-info-total-no-vat", $scope.obj.remainingStyle = "cart-info-remaining-no-vat"), $scope.obj.credits = data.creditsData.usedSum, $scope.obj.total = data.subtotals.final, $scope.obj.remaining = data.creditsData.remainingSum) + }, $scope.$root.$on("alreadyBought", function(event, data) { + CheckOutService.onCheckOutRequested(data) + }), $scope.$root.$on("ownClips", function(event, data) { + CheckOutService.onCheckOutRequested(data) + }) +}), app.controller("CollectionsController", function($scope, BinsModel, Service, LoginModel, ViewStateService) { + $scope.obj = {}, $scope.obj.showImportAll = !1, $scope.obj.showFooter = !1, $scope.obj.showList = !1, $scope.obj.showBin, $scope.obj.addToBin, $scope.obj.addToBinName = "Collections", $scope.obj.collectionsList = COLLECTIONS_LIST_HTML, $scope.loginModel = function() { + return LoginModel.loggedIn + }, $scope.$watch($scope.loginModel, function() { + LoginModel.loggedIn ? $scope.obj.showFooter = !0 : $scope.obj.showFooter = !1 + }), $scope.$root.$on("onBins", function(event) { + $scope.bins = BinsModel.bins, 0 == BinsModel.bins.length && ($scope.obj.addToBinName = "Collections") + }), $scope.$root.$on("active bin changed", function(event) { + $scope.obj.addToBin = BinsModel.addToBin, BinsModel.addToBin && ($scope.obj.addToBinName = getAbbrName(BinsModel.addToBin.name, 10)) + }), $scope.toggleList = function() { + $scope.obj.showList = !$scope.obj.showList + }, $scope.openList = function() { + $scope.obj.showList = !0 + }, $scope.closeList = function() { + $scope.obj.showList = !1 + }, $scope.deleteIconClicked = function(bin) { + $scope.$root.$emit("collection delete requested", [bin]) + }, $scope.showCollectionIconClicked = function(bin) { + BinsModel.showBin = bin, $scope.$root.$emit("bin selected", bin.name), ViewStateService.viewRequested("bins"), $scope.closeList() + }, $scope.collectionNameClicked = function(bin) { + BinsModel.addToBin = bin, $scope.obj.addToBinName = getAbbrName(bin.name, 10), $scope.closeList(), Service.setActiveBin(BinsModel.addToBin.id) + }, $scope.freeItemsClicked = function() { + ViewStateService.viewRequested("freebies"), $scope.closeList() + }, $scope.onClick = function() { + $scope.$root.$emit("select clicked") + }, $scope.onAddClicked = function() { + $scope.$root.$emit("modal add collection requested") + } +}), app.controller("DownloadAllController", function($scope, ViewStateModel, DownloadBatchService, PurchasesModel, AnalyticsService) { + function onStateChange() { + "downloads" === ViewStateModel.getState() && PurchasesModel.purchasesVO && PurchasesModel.purchasesVO.items ? $scope.obj.show = !0 : $scope.obj.show = !1 + } + $scope.obj = { + show: !1, + isDownloading: !1 + }, $scope.$root.$on("on downloading all purchases complete", function(event) { + $scope.$apply(function() { + $scope.obj.isDownloading = !1 + }) + }), $scope.$root.$on("cancel all requested", function(event) { + console.log("DownloadAllController cancel all requested"), $scope.obj.isDownloading = !1 + }), $scope.$root.$on("on purchases vo", function() { + onStateChange() + }), $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, onStateChange, !0), $scope.onDownloadAllClicked = function() { + console.log("DownloadAllController onDownloadAllClicked"), $scope.obj.isDownloading = !0, DownloadBatchService.onBatchRequested(); + var ga = { + ec: "download%20all" + }; + console.log("DownloadAllController ga", ga), AnalyticsService.sendData(ga) + } +}), app.controller("DownloadProgressController", function($scope, $timeout, ProgressService, DownloadRequestService, DownloadCancelService, ViewStateModel, DownloadModel) { + $scope.obj = { + items: [], + isOpen: !1, + progressCloseIcon: PROGRESS_CLOSE_IMG + }, $scope.viewStateModel = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewStateModel, function() { + $scope.obj.view = ViewStateModel.getState() + }), $scope.$root.$on("select clicked", function(event) { + $scope.obj.isOpen = !1 + }), $scope.$root.$on("import all clicked", function(event) { + $scope.obj.isOpen = !0 + }), $scope.$root.$on("open progress", function(event) { + $scope.obj.isOpen || ($scope.obj.isOpen = !0) + }), $scope.$root.$on("clear progress", function(event) { + $scope.obj.items = DownloadModel.itemsDownloadList + }), $scope.$root.$on("added to progress", function(event, data) { + $scope.obj.items = DownloadModel.itemsDownloadList + }), $scope.onProgressIconClicked = function() { + $scope.$root.$emit("progress button clicked") + }, $scope.$root.$on("progress button clicked", function(event) { + $scope.obj.isOpen = !$scope.obj.isOpen + }), $scope.clearListClicked = function() { + $scope.$root.$emit("progress button clicked"), ProgressService.clearCompleteItems(), 0 < $scope.obj.items.length ? $scope.obj.isOpen = !0 : $scope.obj.isOpen = !1 + }, $scope.showClear = function() { + var show = !1; + return $scope.obj.items.forEach(function(item) { + item.completed && (show = !0) + }), !ProgressService.getDownloadingStatus() && 0 < DownloadModel.itemsDownloadList.length && (show = !0), show + }, $scope.isDownloading = function() { + var isDownloading = !1; + return $scope.obj.items.forEach(function(item) { + item.downloading && (isDownloading = !0) + }), ProgressService.getDownloadingStatus() && (show = !0), isDownloading + }, $scope.showMenu = function() { + return 0 < $scope.obj.items.length + }, $scope.cancelAllClicked = function() { + DownloadCancelService.onCancelAll(), $scope.$root.$emit("cancel all requested") + }, $scope.closeClicked = function() { + $scope.$root.$emit("progress button clicked"), console.log("DownloadProgressController closeClicked", $scope.obj.isOpen), $scope.obj.isOpen = !1, console.log("DownloadProgressController closeClicked", $scope.obj.isOpen) + }, $scope.cancelSingleClicked = function(item) { + DownloadCancelService.onCancelSingle(item) + }, $scope.hideTooltip = function() { + $timeout(function() { + $("#clearListButton").trigger("hide") + }, 0) + } +}), app.controller("FilterController", function($scope, Service, SearchModel, ViewStateModel, AnalyticsService) { + $scope.obj = { + filters: ["Best Match", "Popular", "Newest", "Price", "Duration"] + }, $scope.caret = { + direction: "down" + }, $scope.obj.selected = $scope.obj.filters[0], $scope.onChange = function(val) { + var sortID; + switch (console.log("FilterController changed: ", $scope.obj.selected), $scope.obj.selected = val || $scope.obj.selected, $scope.obj.open = !1, $scope.obj.selected) { + case "Best Match": + sortID = 1; + break; + case "ARTIST": + sortID = 2; + break; + case "Newest": + sortID = 6; + break; + case "Duration": + sortID = 5; + break; + case "Popular": + sortID = 8; + break; + case "PAGE VIEWS": + sortID = 10; + break; + case "Price": + sortID = 4 + } + console.log("FilterController sortID: ", sortID), SearchModel.filter = sortID, SearchModel.resultType = "replace", SearchModel.page = "0", Service.search(), window.scrollTo(0, 0); + var ga = {}; + ga.ec = "search%20filter%20" + $scope.obj.selected.replace(/ /g, "%20"), ga.label = SearchModel.query, AnalyticsService.sendData(ga) + }, $scope.setCurrent = function(val) { + $scope.obj.selected = val + }, $scope.toggled = function(open) { + $scope.obj.direction = open ? "dropup" : "down" + } +}), app.controller("FooterLinksController", function($scope, ViewStateModel, CartModel) { + $scope.obj = { + show: !1 + }, $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, function() { + "cart" === ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 + }, !0), $scope.onPromoCodeClicked = function() { + $scope.$root.$emit("modal promo requested") + } +}); +var FreebiesController = function($scope, ViewStateService, FreebiesModel, ViewStateModel, LoginModel, AnalyticsService) { + function onViewStateChange() { + console.log("FreebiesController onViewStateChange:", ViewStateModel.getState()), "freebies" === ViewStateModel.getState() && LoginModel.getLoggedIn() ? $scope.obj.show = !0 : $scope.obj.show = !1 + } + $scope.obj = { + show: !1 + }, $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.loggedIn = function() { + return LoginModel.getLoggedIn() + }, $scope.$watch($scope.viewState, onViewStateChange, !0), $scope.$watch($scope.loggedIn, onViewStateChange), $scope.onFreebiesButtonClicked = function() { + ViewStateService.viewRequested("freebies"), console.log("FreebiesController onFreebiesButtonClicked"); + var ga = { + ec: "freebies" + }; + console.log("FreebiesController ga", ga), AnalyticsService.sendData(ga) + }, $scope.onAddAllFreebiesToCartClicked = function() { + var ids = []; + FreebiesModel.freebiesVO.items.forEach(function(item) { + ids.push(item.id) + }); + var apiObj = { + fn: "modifyCart", + args: [convertArrayToCommaSeperatedString(ids), ""] + }; + $scope.$root.$emit("api call", apiObj), $scope.$root.$emit("modal add to cart") + } +}; +FreebiesController.$inject = ["$scope", "ViewStateService", "FreebiesModel", "ViewStateModel", "LoginModel", "AnalyticsService"], app.controller("ImportCollectionsController", function($scope, DownloadModel, ViewStateModel, BinsModel) { + $scope.obj = { + show: !1, + isImporting: !1 + }, $scope.$root.$on("on importing bin complete", function(event) { + console.log("ImportCollectionsController on importing bin complete"), $scope.$apply(function() { + $scope.obj.isImporting = !1 + }) + }), $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.binsModel = function() { + return BinsModel.binVO + }, $scope.$watch($scope.viewState, function() { + "bins" === ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 + }, !0), $scope.$watch($scope.binsModel, function() { + "bins" === ViewStateModel.getState() && ($scope.obj.show = !0, 0 < BinsModel.binVO.items.length ? $scope.obj.isImporting = !1 : $scope.obj.isImporting = !0) + }, !0), $scope.onImportAllClicked = function() { + $scope.obj.isImporting = !0, $scope.$root.$emit("download requested", BinsModel.binVO.items), $scope.$root.$emit("import all clicked") + } +}), app.controller("IntroAssetsController", function($scope) { + $scope.obj = { + state: 0, + stateName: "" + }, $scope.$root.$on("intro asset requested", function(event, stateObj) { + $scope.obj.stateName = stateObj.stateName, console.log("IntroAssetsController stateName", $scope.obj.stateName); + var fromX, toX, fromY, toY, currArrow = stateObj.arrowClass; + switch (currArrow) { + case ".intro-asset-arrow-up": + fromY = 20, toY = 0; + break; + case ".intro-asset-arrow-left": + fromX = 20, toX = 0; + break; + case ".intro-asset-arrow-down": + fromY = 0, toY = 20 + } + "" != currArrow && ($(currArrow).css("top", "").css("left", "").css("bottom", ""), $(currArrow).css(stateObj.posX[0], stateObj.posX[1]), $(currArrow).css(stateObj.posY[0], stateObj.posY[1]), $(".intro-asset-arrow").velocity("stop"), $scope.loop(currArrow, fromX, toX, fromY, toY)) + }), $scope.loop = function(target, fromX, toX, fromY, toY) { + $(target).velocity({ + translateX: [fromX, toX], + translateY: [fromY, toY] + }, { + duration: 1e3, + loop: !0 + }) + } +}), app.controller("ListItemController", function($scope, VersionsModel, ViewStateModel) { + $scope.obj = {}, $scope.deleteIconClicked = function() { + var apiObj = { + fn: "modifyCart", + args: ["", $scope.item.id] + }; + $scope.$root.$emit("api call", apiObj) + }, $scope.versionButtonClicked = function() { + VersionsModel.setVersions($scope.item.versions) + }, $scope.imageHovered = function(e) { + var item; + "cart" == ViewStateModel.getState() ? item = $scope.item : "downloads" == ViewStateModel.getState() && (item = $scope.item.versions[0]), $scope.$root.$emit("start preview", item) + }, $scope.imageLeft = function(item) { + $scope.$root.$emit("stop preview", item) + } +}), app.controller("ListCartController", function($scope, CartModel) { + $scope.obj = {}, $scope.cartItems = function() { + return CartModel + }, $scope.$watchCollection($scope.cartItems, function() { + CartModel.cartVO && ($scope.obj.items = CartModel.cartVO.items) + }) +}), app.controller("ListDownloadsController", function($scope, PurchasesModel) { + $scope.obj = {}, $scope.purchasedItems = function() { + return PurchasesModel + }, $scope.$watchCollection($scope.purchasedItems, function() { + PurchasesModel.purchasesVO && (console.log("ListController onPurchasesModelChange: ", PurchasesModel.purchasesVO.items), $scope.obj.items = PurchasesModel.purchasesVO.items) + }) +}), app.controller("LoginController", function($scope, LoginModel, UserModel) { + $scope.obj = { + loggedIn: !1, + logo: LOGO_IMG, + logoStyle: "logo-reg" + }, $scope.loginModel = function() { + return LoginModel + }, $scope.userModel = function() { + return UserModel + }, $scope.$watch($scope.loginModel, function() { + void 0 === LoginModel.getLoggedIn() ? $scope.obj.loggedIn = $scope.obj.loggedIn : $scope.obj.loggedIn = LoginModel.getLoggedIn(); + $scope.obj.loggedIn && ($scope.obj.avatarURL = UserModel.getAvatarURL()); + !1 === LoginModel.getLoggedIn() || void 0 === LoginModel.getLoggedIn() ? $scope.obj.row_top_style = "row-top-loggedout" : $scope.obj.row_top_style = "row-top-loggedin" + }, !0), $scope.$watch($scope.userModel, function() { + $scope.obj.avatarURL = UserModel.getAvatarURL(), 0 < THIRD_PARTY.length && ($scope.obj.logo = BASE_URL + "pond5_shared/images/" + THIRD_PARTY + ".png", $scope.obj.logoStyle = "logo-tp") + }, !0), $scope.loginRequested = function() { + $scope.$root.$emit("modal login requested") + }, $scope.logoutClicked = function() { + $scope.$root.$emit("modal logout requested") + } +}), app.controller("MainViewController", function($scope, ViewStateModel, SearchModel) { + $scope.obj = { + tilesClass: "main-content" + }, $scope.$root.$on("filters button clicked", function(event, state) { + $scope.obj.tilesClass = state ? (ViewStateModel.setState("search"), "main-content-advanced-search") : "main-content" + }), $scope.$root.$on("advanced search close requested", function(event) { + $scope.obj.tilesClass = "main-content" + }), $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, function() { + "search" === ViewStateModel.getState() && "add" === SearchModel.resultType ? console.log("MainViewController, do not scroll to top") : window.scrollTo(0, 0); + "cart" !== ViewStateModel.getState() && "downloads" !== ViewStateModel.getState() || ($scope.obj.tilesClass = "main-content"); + $scope.obj.state = ViewStateModel.getState() + }, !0) +}); +var MenuController = function($scope, ViewStateService, AnalyticsService) { + $scope.states = ["default", "hover", "selected"], $scope.btn0 = { + state: $scope.states[2], + selected: !0 + }, $scope.btn1 = { + state: $scope.states[0], + selected: !1 + }, $scope.btn2 = { + state: $scope.states[0], + selected: !1 + }, $scope.btn3 = { + state: $scope.states[0], + selected: !1 + }, $scope.buttons = [$scope.btn0, $scope.btn1, $scope.btn2, $scope.btn3], $scope.click = function(button) { + console.log("MenuController clicked ", button), $scope.selected = button; + for (var i = 0; i < $scope.buttons.length - 1; i++) button === $scope.buttons[i] ? ($scope.buttons[i].selected = !0, $scope.buttons[i].state = $scope.states[2]) : button != $scope.buttons[3] && ($scope.buttons[i].selected = !1, $scope.buttons[i].state = $scope.states[0]); + var view; + switch (button) { + case $scope.buttons[0]: + view = "search"; + break; + case $scope.buttons[1]: + view = "downloads"; + break; + case $scope.buttons[2]: + view = "previews"; + break; + case $scope.buttons[3]: + view = "settings" + } + console.log("MenuController clicked view ", view), $scope.requestView(view) + }, $scope.requestView = function(view) { + "settings" === view ? $scope.$root.$emit("modal add destination requested") : ViewStateService.viewRequested(view); + var ga = {}; + ga.ec = view, console.log("MenuController ga", ga), AnalyticsService.sendData(ga) + }, $scope.over = function(button) { + console.log("MenuController over ", button), button.selected || (button.state = $scope.states[1]) + }, $scope.out = function(button) { + console.log("MenuController over ", button), button.selected || (button.state = $scope.states[0]) + } +}; +MenuController.$inject = ["$scope", "ViewStateService", "AnalyticsService"], app.controller("MessageController", function($scope, ViewStateModel) { + $scope.obj = { + show: !1 + }, $scope.$root.$on("message view requested", function(event, show, data, list, imgUrl) { + $scope.obj.title = null, $scope.obj.messageList = null, $scope.obj.message = null, $scope.obj.imgUrl = null, $scope.obj.showImg = !1, ($scope.obj.show = show) && ($scope.obj.title = data[0], list ? $scope.obj.messageList = data[1] : $scope.obj.message = data[1], 2 === data.length ? $scope.obj.label = "OK" : $scope.obj.label = data[2], imgUrl && ($scope.obj.imgUrl = imgUrl, $scope.obj.showImg = !0)) + }), $scope.viewStateModel = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewStateModel, function() { + "search" !== ViewStateModel.getState() && ($scope.obj.show = !1) + }) +}), app.controller("ModalAddCollectionConfirmationController", function($scope, $uibModal, BinsModel) { + $scope.items = [], $scope.$root.$on("collection created", function(event, data) { + console.log("ModalAddCollectionConfirmationController event handler", data), $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_ADD_COLLECTION_CONFIRMATION_HTML, + controller: ModalAddCollectionConfirmationInstanceCtrl, + size: size, + resolve: { + items: function() { + return $scope + } + } + }).result.then(function() { + console.log("ModalAddCollectionConfirmationController OK") + }, function() { + console.log("ModalAddCollectionConfirmationController CANCELED") + }) + } +}); +var ModalAddCollectionConfirmationInstanceCtrl = function($scope, $uibModalInstance, items, BinsModel) { + $scope.obj = { + title: "Complete!", + messagePre: "Your collection '", + messagePost: "' was succesfully created", + newBinName: BinsModel.newBinName + }, $scope.ok = function() { + $uibModalInstance.dismiss("cancel") + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalAddCollectionController", function($scope, $uibModal, Service, UserModel, BinsModel) { + $scope.items = [], $scope.$root.$on("modal add collection requested", function(event) { + console.log("ModalAddCollectionController event handler"), $scope.open("sm") + }), $scope.open = function(size) { + var modalInstance = $uibModal.open({ + templateUrl: MODAL_ADD_COLLECTION_HTML, + controller: ModalAddCollectionInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + items: function() { + return $scope + } + } + }); + modalInstance.result.then(function() { + console.log("ModalAddCollectionController OK") + }, function() { + console.log("ModalAddCollectionController CANCELED") + }), modalInstance.result.then(function(result) {}, function(result) {}) + } +}); +var ModalAddCollectionInstanceCtrl = function($scope, $uibModalInstance, items, Service, BinsModel) { + $scope.obj = { + showMessage: !1 + }, $scope.create = function() { + console.log("ModalAddCollectionInstanceCtrl bin name: ", document.getElementById("addCollectionInput").value); + var binName = document.getElementById("addCollectionInput").value; + 1 < binName.length && ($uibModalInstance.close(), BinsModel.newBinName = binName, Service.createBin(binName)) + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalAddToCartController", function($scope, $uibModal, Service, ViewStateService) { + $scope.$root.$on("modal add to cart", function(event) { + console.log("ModalAddToCartController event handler"), $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_ADD_TO_CART_HTML, + controller: ModalAddToCartInstanceCtrl, + size: size + }).result.then(function() { + console.log("ModalAddToCartController proceed"), ViewStateService.viewRequested("cart") + }, function() { + console.log("ModalAddToCartController later") + }) + } +}); +var ModalAddToCartInstanceCtrl = function($scope, $uibModalInstance) { + $scope.onProceed = function() { + console.log("ModalAddToCartInstanceCtrl onProceed"), $uibModalInstance.close() + }, $scope.onCancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalBillingAddressController", function($scope, $uibModal) { + $scope.obj = {}, $scope.$root.$on("modal billing address requested", function(event) { + console.log("ModalBillingAddressController event handler"), $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_BILLING_ADDRESS_HTML, + controller: ModalBillingAddressInstanceCtrl, + size: size, + windowClass: "modal-billing-address", + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + console.log("ModalBillingAddressController OK") + }, function() { + console.log("ModalBillingAddressController CANCELED"), $scope.$root.$emit("billing info canceled") + }) + } +}); +var ModalBillingAddressInstanceCtrl = function($scope, $uibModalInstance, obj, Service) { + $scope.firstName = "", $scope.lastName = "", $scope.street1 = "", $scope.street2 = "", $scope.province = "", $scope.zipCode = "", $scope.city = "", $scope.state = "", $scope.country = "", $scope.error = !1, $scope.countries = COUNTRIES, $scope.states = STATES, $scope.submit = function(myForm) { + if (console.log("ModalBillingAddressInstanceCtrl ok: ", myForm.firstName.$modelValue, myForm.lastName.$modelValue), console.log("ModalBillingAddressInstanceCtrl form valid: ", myForm.$valid), myForm.$valid) { + var stateCode; + stateCode = "" == myForm.state.$modelValue ? "" : myForm.state.$modelValue.code; + var data = { + country: myForm.country.$modelValue.code, + firstName: myForm.firstName.$modelValue, + lastName: myForm.lastName.$modelValue, + organization: myForm.organization.$modelValue, + department: myForm.department.$modelValue, + companyID: myForm.companyID.$modelValue, + vatID: myForm.vatID.$modelValue, + street1: myForm.street1.$modelValue, + street2: myForm.street2.$modelValue, + province: myForm.province.$modelValue, + zipCode: myForm.zipCode.$modelValue, + city: myForm.city.$modelValue, + state: stateCode + }; + console.log("ModalBillingAddressInstanceCtrl DATA", data); + var apiObj = { + fn: "setBillingAddress", + args: [data] + }; + $scope.$root.$emit("api call", apiObj), $uibModalInstance.dismiss() + } else console.log("ModalBillingAddressInstanceCtrl form is not valid"), $scope.error = !0 + }, $scope.close = function() { + $uibModalInstance.dismiss() + }, $scope.back = function() { + $uibModalInstance.dismiss(), $scope.$root.$emit("on modal choose billing info requested") + } +}; +app.controller("ModalBuyCreditsController", function($scope, $uibModal, ViewStateModel) { + $scope.obj = {}, $scope.$root.$on("modal buy credits requested", function() { + console.log("ModalBuyCreditsController event handler"), $scope.obj.title = "", $scope.obj.message = "As a reminder, only credits purchased in $USD can be used in this Add-on."; + $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_BUY_CREDITS_HTML, + controller: ModalBuyCreditsInstanceCtrl, + size: size, + resolve: { + obj: function() { + return $scope.obj + } + }, + windowClass: "modal-small" + }).result.then(function() { + console.log("ModalBuyCreditsController OK"), ViewStateModel.allowPreviews = !0, opn("https://www.pond5.com/credit-packages") + }, function() { + console.log("ModalBuyCreditsController CANCELED") + }) + } +}); +var ModalBuyCreditsInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.ok = function() { + console.log("ModalBuyCreditsInstanceCtrl OK"), $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel"), console.log("ModalBuyCreditsInstanceCtrl cancel") + } +}; +app.controller("ModalChooseBillingInfoController", function($scope, $uibModal, BillingInfoModel, CheckOutService, Service) { + $scope.items = [], $scope.obj = {}, $scope.$root.$on("on modal choose billing info requested", function(event) { + console.log("ModalChooseBillingInfoController event handler: ", BillingInfoModel.getBillingInfo()), $scope.items = BillingInfoModel.getBillingInfo(), $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_CHOOSE_BILLING_INFO_HTML, + controller: ModalChooseBillingInfoInstanceCtrl, + windowClass: "modal-choose-billing", + size: size, + resolve: { + items: function() { + return $scope.items + } + } + }).result.then(function(item) { + console.log("ModalChooseBillingInfoController ok, selected: ", item.addressid), CheckOutService.onCheckOutRequested() + }, function() { + console.log("ModalChooseBillingInfoController dismissed"), $scope.$root.$emit("billing info canceled") + }) + } +}); +var ModalChooseBillingInfoInstanceCtrl = function($scope, $uibModalInstance, items, BillingInfoModel, Service) { + console.log("ModalChooseBillingInfoInstanceCtrl items", items), console.log("ModalChooseBillingInfoInstanceCtrl default", BillingInfoModel.getDefaultInfo()), $scope.items = items, $scope.selected = BillingInfoModel.getDefaultInfo(), $scope.adyenEncryption = "https://plugin.pond5.com/pond5_shared/images/adyen-encryption.png", $scope.onRbClicked = function(item) { + $scope.selected = item, console.log("ModalChooseBillingInfoInstanceCtrl rb > default", item), BillingInfoModel.setDefaultInfo(item), Service.getCartTotal() + }, $scope.onOKClicked = function() { + $uibModalInstance.close($scope.selected) + }, $scope.close = function() { + $uibModalInstance.dismiss() + }, $scope.addNewClicked = function() { + $uibModalInstance.dismiss(), $scope.$root.$emit("modal billing address requested") + }, $scope.readAgreement = function() { + console.log("ModalChooseBillingInfoInstanceCtrl readAgreement"), opn("https://www.pond5.com/legal/license") + }, $scope.helpCenter = function() { + opn("https://help.pond5.com/hc/en-us/") + }, $scope.callUs = function() { + opn("https://help.pond5.com/hc/en-us/requests/new") + } +}; +app.controller("ModalChooseFormatController", function($scope, $uibModal) { + $scope.items = [], $scope.$root.$on("on add to cart clicked", function(event, formats) { + console.log("ModalChooseFormatController handler, formats: ", formats), $scope.items = [], $scope.items = formats, $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_CHOOSE_FORMAT_HTML, + controller: ModalChooseFormatInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + items: function() { + return $scope.items + } + } + }).result.then(function() {}, function() { + console.log("ModalChooseFormatController dismissed") + }) + } +}); +var ModalChooseFormatInstanceCtrl = function($scope, $uibModalInstance, items, Service) { + $scope.items = items, $scope.items[0].selected = !0, $scope.onRbClicked = function(item, index) { + console.log("ModalChooseFormatInstanceCtrl onRbClicked: " + item + "-" + index); + for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected = index === i + }, $scope.onAddToCartClicked = function() { + for (var i = 0; i < $scope.items.length; i++) + if ($scope.items[i].selected) { + var item = $scope.items[i], + apiObj = { + fn: "modifyCart", + args: [item.id + ":" + item.offset] + }; + $scope.$root.$emit("api call", apiObj) + } $uibModalInstance.dismiss() + } +}; +app.controller("ModalChooseVersionController", function($scope, $uibModal, Service, DownloadModel) { + $scope.items = [], $scope.$root.$on("on versions selected", function(event, versions) { + console.log("ModalChooseVersionController event handler: ", $scope.items, versions), $scope.items = [], $scope.items = versions, $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_CHOOSE_VERSION_HTML, + controller: ModalChooseVersionInstanceCtrl, + size: size, + resolve: { + items: function() { + return $scope.items + } + }, + windowClass: "modal-small" + }).result.then(function(selectedIndex) { + var selectedItem = $scope.items[selectedIndex]; + DownloadModel.selectedVersion = selectedIndex, Service.getPurchaseURL(selectedItem.id, selectedItem.transactionID, selectedItem.versionID, selectedItem.version) + }, function() { + console.log("ModalChooseVersionController dismissed") + }) + } +}); +var ModalChooseVersionInstanceCtrl = function($scope, $uibModalInstance, items) { + $scope.items = items, $scope.selected = $scope.items[0], $scope.selectedIndex = 0, $scope.onRbClicked = function(index) { + $scope.selected = $scope.items[index], $scope.selectedIndex = index + }, $scope.ok = function() { + $uibModalInstance.close($scope.selectedIndex) + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalClearCartConfirmationController", function($scope, $uibModal) { + $scope.obj = [], $scope.$root.$on("clear cart requested", function(event, data, size) { + console.log("ModalClearCartConfirmationController event handler", data), $scope.obj.title = "Clear My Cart", $scope.obj.message = "Are you sure you want to clear your cart?", $scope.obj.itemsToDelete = data[0], $scope.obj.label = "CLEAR", $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", size = size || "sm", $scope.open(size) + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_SIMPLE_HTML, + controller: ModalClearCartConfirmationInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + console.log("ModalClearCartConfirmationController OK"); + var apiObj = { + fn: "modifyCart", + args: ["", $scope.obj.itemsToDelete] + }; + $scope.$root.$emit("api call", apiObj) + }, function() { + console.log("ModalClearCartConfirmationController CANCELED") + }) + } +}); +var ModalClearCartConfirmationInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalDeleteCollectionConfirmationController", function($scope, $uibModal, Service, ViewStateModel, BinsModel, ViewStateService) { + $scope.obj = {}, $scope.$root.$on("collection delete requested", function(event, data, size) { + console.log("ModalDeleteCollectionConfirmationController event handler", data, data.length, size), $scope.obj.title = "Delete Collection", $scope.obj.message = "Are you sure you want to delete the collection " + data[0].name + "?", $scope.obj.bin = data[0], $scope.obj.label = "DELETE", $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", size = size || "sm", $scope.open(size) + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_SIMPLE_HTML, + controller: ModalDeleteCollectionConfirmationInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + BinsModel.selectedBin == $scope.obj.bin && ViewStateService.viewRequested("search"), Service.removeBin($scope.obj.bin.id), ViewStateModel.allowPreviews = !0 + }, function() {}) + } +}); +var ModalDeleteCollectionConfirmationInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalFreebiesController", function($scope, $uibModal, ViewStateService) { + $scope.$root.$on("modal freebies", function(event) { + console.log("ModalFreebiesController event handler"), $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_FREEBIES_HTML, + controller: ModalFreebiesInstanceCtrl, + size: size + }).result.then(function() { + console.log("ModalFreebiesController OK"), ViewStateService.viewRequested("freebies") + }, function() { + console.log("ModalFreebiesController dismissed") + }) + } +}); +var ModalFreebiesInstanceCtrl = function($scope, $uibModalInstance) { + $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalLoginController", function($scope, $uibModal) { + $scope.obj = {}, $scope.$root.$on("modal login requested", function(event) { + console.log("ModalLoginController event handler"), $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_LOGIN_HTML, + controller: ModalLoginInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + console.log("ModalLoginController OK") + }, function() { + console.log("ModalLoginController CANCELED") + }) + } +}); +var ModalLoginInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.userName = obj.userName, $scope.obj.password = obj.password, $scope.obj.showTitle = !0, $scope.obj.showClose = !0, $scope.loginRequested = function() { + $uibModalInstance.close(); + var apiObj = { + fn: "login", + args: [$scope.obj.userName, $scope.obj.password] + }; + $scope.$root.$emit("api call", apiObj) + }, $scope.close = function() { + $uibModalInstance.dismiss("cancel") + }, $scope.signUp = function() { + opn("https://www.pond5.com/login") + } +}; +app.controller("ModalLogoutConfirmationController", function($scope, $uibModal, Service, ViewStateModel) { + $scope.obj = {}, $scope.$root.$on("modal logout requested", function(event, data, size) { + console.log("ModalLogoutConfirmationController event handler"), $scope.obj.title = "Log out", $scope.obj.message = "Are you sure you want to log out?", $scope.obj.label = "YES", $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", size = size || "sm", $scope.open(size) + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_SIMPLE_HTML, + controller: ModalLogoutConfirmationInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + Service.logout(), ViewStateModel.allowPreviews = !0 + }, function() {}) + } +}); +var ModalLogoutConfirmationInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalNotLoggedInController", function($scope, $uibModal) { + $scope.obj = {}, $scope.$root.$on("modal not logged in", function(event, data) { + $scope.obj.title = data[0], $scope.obj.message = "You're not logged in", $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_NOT_LOGGED_IN_HTML, + controller: ModalNotLoggedInInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + console.log("ModalNotLoggedInController OK") + }, function() { + console.log("ModalNotLoggedInController CANCELED") + }) + } +}); +var ModalNotLoggedInInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.loginRequested = function() { + $uibModalInstance.dismiss("cancel"), $scope.$root.$emit("modal login requested") + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + }, $scope.signUp = function() { + opn("https://www.pond5.com/login") + } +}; +app.controller("ModalPromoCodeController", function($scope, $uibModal, Service, UserModel) { + $scope.items = [], $scope.obj = { + label: "APPLY", + onlyNumbers: /^\d+$/ + }, $scope.$root.$on("modal promo requested", function(event) { + console.log("ModalPromoCodeController event handler"), $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_PROMO_CODE_HTML, + controller: ModalPromoCodeInstanceCtrl, + size: size, + windowClass: "modal-small", + resolve: { + items: function() { + return $scope + } + } + }).result.then(function() { + console.log("ModalPromoCodeController OK") + }, function() { + console.log("ModalPromoCodeController CANCELED") + }) + } +}); +var ModalPromoCodeInstanceCtrl = function($scope, $uibModalInstance, items, Service, $filter) { + $scope.obj = { + showMessage: !1, + label: "APPLY", + onlyNumbers: /^\d+$/ + }, $scope.$root.$on("promo code added", function(event, data) { + var message; + console.log("ModalPromoCodeController event handler", data), message = data.commands[0].sum ? $filter("currency")(data.commands[0].sum) + " were succesfully added to your account!" : "Invalid code. Please try again or contact Pond5.", $scope.obj.credits = data, $scope.obj.showMessage = !0, $scope.obj.message = message, $scope.obj.label = "OK" + }), $scope.codeApplied = function() { + if (console.log("ModalPromoCodeInstanceCtrl codeApplied: ", document.getElementById("promoInput").value), "OK" == $scope.obj.label) $uibModalInstance.close(); + else { + var code = document.getElementById("promoInput").value; + 1 < code.length && Service.promoRedeem(code) + } + }, $scope.ok = function() { + console.log("ModalPromoCodeInstanceCtrl OK"), $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalRemoveCollectionController", function($scope, $uibModal, Service, BinsModel, ViewStateModel) { + $scope.items = [], $scope.showModal = function() { + return BinsModel.showModal + }, $scope.$root.$on("modal remove collection requested", function(event) { + console.log("ModalRemoveCollectionController remove collection requested event handler", BinsModel.showModal, BinsModel.clipClicked), $scope.items = BinsModel.bins, 0 < $scope.items.length && $scope.open() + }), $scope.$root.$on("collection removed", function(event) { + console.log("ModalAddCollectionController collection removed event handler") + }), $scope.open = function(size) { + var modalInstance = $uibModal.open({ + templateUrl: MODAL_REMOVE_COLLECTION_HTML, + controller: ModalRemoveCollectionInstanceCtrl, + windowClass: "modal-fit", + resolve: { + items: function() { + return $scope.items + } + } + }); + $scope.resetBins = function() { + BinsModel.showModal = !1; + for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected = !1 + }, modalInstance.result.then(function() { + console.log("OK: ", BinsModel.clipClicked, $scope.items); + for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected && (console.log("ModalRemoveCollectionController selected bin:", $scope.items[i].id), Service.removeBin($scope.items[i].id)); + $scope.resetBins(), ViewStateModel.allowPreviews = !0 + }, function() { + $scope.resetBins() + }) + } +}); +var ModalRemoveCollectionInstanceCtrl = function($scope, $uibModalInstance, items) { + $scope.items = items, $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalReplaceController", function($scope, $uibModal, ReplaceModel, ReplaceServiceShared) { + $scope.items = [], $scope.$root.$on("modal replace", function(event, items) { + console.log("ModalReplaceController event handler: ", items), $scope.items = items, $scope.open("lg") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_REPLACE_HTML, + controller: ModalReplaceInstanceCtrl, + size: size, + resolve: { + items: function() { + return $scope.items + } + }, + windowClass: "modal-replace" + }).result.then(function() { + ReplaceServiceShared.onModalReplaceOK() + }, function() { + ReplaceModel.setState(DEFAULT) + }) + } +}); +var ModalReplaceInstanceCtrl = function($scope, $uibModalInstance, items) { + $scope.obj = { + checkIcon: "https://plugin.pond5.com/pond5_shared/images/check-icon.png", + modalHeader: MODAL_REPLACE_HEADER, + modalContent: MODAL_REPLACE_CONTENT, + resTitle: MODAL_REPLACE_RES_TITLE + }, $scope.items = items; + for (var i = 0; i < $scope.items.length; i++) { + $scope.items[i].selected = !0; + for (var j = 0; j < $scope.items[i].formats.length; j++) console.log("ModalReplaceInstanceCtrl incart: ", $scope.items[i].formats[j].inDownloads), $scope.items[i].formats[j].inDownloads && ($scope.items[i].formats.length = 0), 0 < $scope.items[i].formats.length && $scope.items[i].formats[j].inCart && ($scope.items[i].formats[j].selected = !0, $scope.items[i].oneFormatInCart = !0); + !$scope.items[i].oneFormatInCart && 0 < $scope.items[i].formats.length && ($scope.items[i].formats[0].selected = !0) + } + $scope.selectAllClicked = function() { + var item; + console.log("ModalReplaceInstanceCtrl selectAllClicked: ", $scope.obj.selectAll); + for (var i = 0; i < $scope.items.length; i++) item = $scope.items[i], !$scope.obj.selectAll || item.inCart || item.inDownloads ? item.selected = !0 : item.selected = !1 + }, $scope.onRbClicked = function(item, index) { + console.log("ModalReplaceInstanceCtrl onRbClicked: " + item.name + "-" + item.selected); + for (var i = 0; i < item.formats.length; i++) item.formats[i].selected = index === i + }, $scope.onCbClicked = function(item, index) { + console.log("ModalReplaceInstanceCtrl onCbClicked: " + item.name + "-" + item.selected), item.selected = !item.selected; + for (var i = 0; i < item.formats.length; i++) item.formats[i].selected = index === i; + console.log("ModalReplaceInstanceCtrl onCbClicked after toggle: " + item.name + "-" + item.selected) + }, $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalReplaceWarningController", function($scope, $uibModal, Service, DownloadModel, ViewStateService, ReplaceModel) { + $scope.obj = {}, $scope.obj.requestedState = "", $scope.$root.$on("modal replace warning", function(event, viewState) { + console.log("ModalReplaceWarningController event handler, event: ", event), console.log("ModalReplaceWarningController event handler, viewState: ", viewState), $scope.obj.requestedState = viewState, $scope.obj.message = "Visiting the " + viewState + " view will cancel the process of replacing your lo-res previews with hi-res clips. Are you sure you want to visit the " + viewState + " view?", $scope.open("sm") + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_REPLACE_WARNING_HTML, + controller: ModalReplaceWarningInstanceCtrl, + size: size, + resolve: { + obj: function() { + return $scope.obj + } + }, + windowClass: "modal-small" + }).result.then(function() { + ViewStateService.onViewApproved(!0) + }, function() { + console.log("ModalReplaceWarningController CANCELED"), ViewStateService.onViewApproved(!1) + }) + } +}); +var ModalReplaceWarningInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("ModalSimpleController", function($scope, $uibModal, Service, DownloadModel, ViewStateModel) { + $scope.obj = { + imgUrl: "", + showImg: !1 + }, $scope.$root.$on("modal simple requested", function(event, data, size, list, imgUrl) { + var windowClass; + $scope.obj.title = null, $scope.obj.messageList = null, $scope.obj.message = null, $scope.obj.imgUrl = null, $scope.obj.showImg = !1, list ? $scope.obj.messageList = data[1] : $scope.obj.message = data[1], 2 === data.length ? $scope.obj.label = "OK" : $scope.obj.label = data[2], imgUrl && ($scope.obj.imgUrl = imgUrl, $scope.obj.showImg = !0), "sm" === size ? windowClass = "modal-small" : "lg" === size && (windowClass = "modal-large"), $scope.open(windowClass) + }), $scope.open = function(size) { + $uibModal.open({ + templateUrl: MODAL_SIMPLE_HTML, + controller: ModalSimpleInstanceCtrl, + windowClass: size, + resolve: { + obj: function() { + return $scope.obj + } + } + }).result.then(function() { + ViewStateModel.allowPreviews = !0 + }, function() {}) + } +}); +var ModalSimpleInstanceCtrl = function($scope, $uibModalInstance, obj) { + $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.messageList = obj.messageList, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.imgUrl = obj.imgUrl, $scope.ok = function() { + $uibModalInstance.close() + }, $scope.cancel = function() { + $uibModalInstance.dismiss("cancel") + } +}; +app.controller("PreviewAudioController", function($scope, ViewStateModel) { + $scope.obj = { + show: !1 + }, $scope.$root.$on("start preview", function(event, item, xpos) { + if (("Music" == item.type || "Sound effect" == item.type) && ViewStateModel.allowPreviews) { + var num = Number(item.dur), + seconds = Math.floor(num / 1e3), + minutes = Math.floor(seconds / 60); + 1 === (seconds = seconds - 60 * minutes).toString().length && (seconds = "0" + seconds); + var format = minutes + ":" + seconds; + $scope.obj.dur = format, item.dur || ($scope.obj.dur = ""), $scope.obj.timer = setTimeout(function() { + document.getElementById("tracktime").style.left = "0px", $scope.playAudio(item.m4aURL, xpos), $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps ? $scope.obj.artist = "" : $scope.obj.artist = item.fps, $scope.obj.iconLargeURL = item.iconLargeURL, item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), $scope.$apply(function() { + $scope.obj.show = !0 + }) + }, 400) + } + }), $scope.$root.$on("stop preview", function(event, data) { + data && (clearTimeout($scope.obj.timer), setTimeout(function() { + $scope.playAudio("") + }, 200), $scope.obj.name = "", $scope.obj.price = "", $scope.obj.type = "", $scope.obj.dur = "", $scope.obj.show = !1) + }), $scope.playAudio = function(url, xpos) { + var audio = document.getElementById("audio"); + document.getElementById("source-audio").setAttribute("src", url), audio.load() + } +}), app.controller("PreviewPhotoController", function($scope, ViewStateModel) { + $scope.obj = { + show: !1, + showInfo: !0 + }, $scope.$root.$on("start preview", function(event, item, xpos) { + "Photo" != item.type && "Illustration" != item.type || ViewStateModel.allowPreviews && ($scope.obj.timer = setTimeout(function() { + $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps ? $scope.obj.artist = "" : $scope.obj.artist = item.fps, $scope.obj.vs = item.vs, $scope.obj.ar = item.ar, $scope.obj.audioCodec = item.audioCodec, $scope.obj.videoCodec = item.videoCodec, item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), item.ox ? $scope.obj.res = item.ox + " x " + item.oy : $scope.obj.res = "", $scope.obj.type = item.type, $scope.obj.iconLargeURL = item.iconLargeURL; + var size = convertAspectRatio(370, 208, item.aq); + actualRatio = item.aq, targetRatio = size.x / size.y, adjustmentRatio = targetRatio / actualRatio; + var photo = document.getElementById("photo"); + photo.width = size.x, photo.height = size.y, document.getElementById("preview-loading").style.visibility = "hidden", photo.style.position = "absolute"; + var x_pos = 185 - photo.width / 2; + photo.style.left = x_pos + "px", $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps ? $scope.obj.artist = "" : $scope.obj.artist = item.fps, $scope.obj.fps = item.fps, $scope.obj.vs = item.vs, $scope.obj.ar = item.ar, $scope.obj.audioCodec = item.audioCodec, $scope.obj.videoCodec = item.videoCodec, item.videoCodec && -1 != item.videoCodec.indexOf("Apple ProRes") && ($scope.obj.videoCodec = "Apple ProRes"), item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), item.ox ? $scope.obj.res = item.ox + " x " + item.oy : $scope.obj.res = "", $scope.$apply(function() { + $scope.obj.show = !0 + }) + }, 400)) + }), $scope.$root.$on("stop preview", function(event, item) { + item && (clearTimeout($scope.obj.timer), $scope.obj.name = "", $scope.obj.price = "", $scope.obj.type = "", $scope.obj.show = !1) + }) +}), app.controller("PreviewVideoController", function($scope, ViewStateModel) { + $scope.obj = { + show: !1, + timer: null, + item: null, + showInfo: !0 + }, $scope.$root.$on("start preview", function(event, item) { + "Video" != item.type && "AE" != item.type || ViewStateModel.allowPreviews && ($scope.obj.timer = setTimeout(function() { + $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps && ($scope.obj.artist = ""), $scope.obj.fps = item.fps, $scope.obj.vs = item.vs, $scope.obj.ar = item.ar, $scope.obj.audioCodec = item.audioCodec, $scope.obj.videoCodec = item.videoCodec, item.videoCodec && -1 != item.videoCodec.indexOf("Apple ProRes") && ($scope.obj.videoCodec = "Apple ProRes"), item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), item.ox ? $scope.obj.res = item.ox + " x " + item.oy : $scope.obj.res = "", $scope.$apply(function() { + $scope.obj.show = !0 + }), $scope.playVideo(item) + }, 400)) + }), $scope.$root.$on("stop preview", function(event, data) { + clearTimeout($scope.obj.timer), $("#video-frame").children().filter("video").each(function() { + this.pause(), $(this).remove() + }), $("#video-frame").empty(), $scope.obj.name = "", $scope.obj.price = "", $scope.obj.fps = "", $scope.obj.vs = "", $scope.obj.show = !1, document.getElementById("preview-loading").style.visibility = "visible" + }), $scope.playVideo = function(item) { + $("#video-frame").append($("")); + var video = document.getElementsByTagName("video")[0], + source = document.getElementById("source-video"); + video.style.visibility = "hidden"; + var size = convertAspectRatio(370, 208, item.aq); + video.addEventListener("loadedmetadata", function(event) { + video.width = size.x, video.height = size.y, document.getElementById("preview-loading").style.visibility = "hidden", video.style.visibility = "visible" + }), item.h264URL ? (video.pause(), source.setAttribute("src", ""), source.setAttribute("src", item.h264URL), video.load()) : (source.setAttribute("src", ""), video.pause()) + }, $scope.$root.$on("preview info icon over", function() { + $scope.obj.showInfo = !0 + }), $scope.$root.$on("preview info icon out", function() { + $scope.obj.showInfo = !1 + }) +}), app.controller("ReplaceController", function($scope, $timeout, ViewStateModel, ReplaceService, LoginModel, AnalyticsService, ReadClipsOnFSService) { + $scope.obj = { + show: !1, + disabled: !1, + buttonLabel: BUTTON_REPLACE_LABEL, + buttonTooltip: BUTTON_REPLACE_TOOLTIP + }, $scope.$root.$on("replacing complete", function() { + $scope.obj.disabled = !1 + }), $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, function() { + "cart" != ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 + }, !0), $scope.onReplaceButtonClicked = function() { + if (LoginModel.getLoggedIn()) { + $scope.hideTooltip(), $scope.obj.disabled = !0, ReadClipsOnFSService.listPurchasesOnFS(function() { + console.log("DragAndDropController fs items listed, call onClipsFSCollected"), ReplaceService.onClipFSCollected() + }); + var ga = { + ec: "replace%20with%20hires" + }; + AnalyticsService.sendData(ga) + } else $scope.$root.$emit("modal not logged in", [ERROR]) + }, $scope.onReplaceButtonOver = function() { + $timeout(function() { + $("#replaceButton").trigger("show") + }, 0) + }, $scope.onReplaceButtonOut = function() { + $scope.hideTooltip() + }, $scope.hideTooltip = function() { + $timeout(function() { + $("#replaceButton").trigger("hide") + }, 0) + } +}), app.controller("SearchController", function($scope, ViewStateService, SearchModel, ViewStateModel, AnalyticsService) { + $scope.obj = { + filters: MEDIA_TYPES, + direction: "down", + showFilters: !1, + view: "search", + styleInput: "search-input-reg" + }, $scope.viewStateModel = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewStateModel, function() { + $scope.obj.view = ViewStateModel.getState(), 0 < THIRD_PARTY.length && ($scope.obj.styleInput = "search-input-tp") + }, !0), resizePanel = function() { + var numOfTotalResults = SearchModel.searchResultItems.length, + numOfResults = SearchModel.numOfResults, + rect = window.innerWidth * window.innerHeight; + 0 < numOfResults && numOfResults != numOfTotalResults && numOfTotalResults < rect / 25e3 && "search" == ViewStateModel.getState() && (SearchModel.isSearching || (console.log("SearchController resize, new search"), SearchModel.isSearching = !0, SearchModel.resultType = "add", SearchModel.page = SearchModel.page + 1, ViewStateService.viewRequested("search"))) + }, $scope.obj.selected = $scope.obj.filters[0], $scope.$root.$on("filters button clicked", function(event, state) { + $scope.obj.showFilters = state + }), $scope.filtersRequested = function() { + $scope.obj.showFilters = !$scope.obj.showFilters, $scope.$root.$emit("filters button clicked", $scope.obj.showFilters) + }, $scope.onChange = function(val) { + var sortID; + switch (console.log("SearchController onChange: ", val), $scope.obj.selected = val, $scope.obj.open = !1, $scope.obj.selected) { + case "Footage": + sortID = BM_VIDEO; + break; + case "After Effects": + sortID = BM_AFTER_EFFECTS; + break; + case "Music": + sortID = BM_MUSIC; + break; + case "SFX": + sortID = BM_SFX; + break; + case "Public Domain": + sortID = BM_PUBLIC_DOMAIN; + break; + case "Photos": + sortID = BM_PHOTO; + break; + case "Illustrations": + sortID = BM_ILLUSTRATIONS + } + SearchModel.sumOfBitmasks = sortID, console.log("SearchController changed, selected, bm: ", SearchModel.sumOfBitmasks), $scope.$root.$emit("media filter change", sortID), $scope.search() + }, $scope.setCurrent = function(val) { + $scope.obj.selected = val + }, $scope.toggled = function(open) { + $scope.obj.direction = open ? "dropup" : "down" + }, $scope.search = function() { + var query = document.getElementById("search").value; + "Search Pond5..." === query && (query = ""); + var ga = { + ec: "search" + }; + ga.ea = $scope.obj.selected.replace(/ /g, "%20"), ga.el = query.replace(/ /g, "%20"), AnalyticsService.sendData(ga), SearchModel.query = query, SearchModel.resultType = "replace", SearchModel.page = 0, SearchModel.sumOfBitmasks === BM_PUBLIC_DOMAIN && (SearchModel.query = SearchModel.query + " editorial:1"), console.log("SearchController search: ", query, SearchModel.sumOfBitmasks, SearchModel.resultType, SearchModel.page), ViewStateService.viewRequested("search") + }, $scope.searchButtonClicked = function() { + $scope.search() + }, $scope.enterThis = function() { + 13 === event.keyCode && $scope.search() + }, $scope.onSearchIconClicked = function() { + ViewStateService.viewRequested("search") + } +}); +var SellController = function($scope, AnalyticsService) { + $scope.sellClicked = function() { + var ga = { + ec: "sell%20media" + }; + console.log("SellController ga", ga), AnalyticsService.sendData(ga), opn("https://www.pond5.com/index.php?page=my_uploads") + } +}; +SellController.$inject = ["$scope", "AnalyticsService"], app.controller("SidebarController", function($scope, ViewStateModel, ViewStateService, AnalyticsService) { + $scope.obj = { + view: "search" + }, $scope.viewStateModel = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewStateModel, function() { + $scope.obj.view = ViewStateModel.getState() + }), $scope.onDownloadsIconClicked = function() { + $scope.$root.$emit("views requested", "downloads"), ViewStateService.viewRequested("downloads"); + var ga = { + ec: "downloads" + }; + AnalyticsService.sendData(ga) + }, $scope.onPreviewsIconClicked = function() { + ViewStateService.viewRequested("previews"); + var ga = { + ec: "imported%20previews" + }; + AnalyticsService.sendData(ga) + }, $scope.onDestinationIconClicked = function() { + $scope.$root.$emit("modal add destination requested"); + var ga = { + ec: "add%20destination" + }; + AnalyticsService.sendData(ga) + } +}), app.controller("SubTopRowController", function($scope, ViewStateModel, BinsModel, SearchModel, CartModel, PurchasesModel, UserModel, AnalyticsService) { + function onViewStateChange() { + var title; + switch (ViewStateModel.getState()) { + case "downloads": + title = "MY DOWNLOADS"; + break; + case "previews": + title = "MY IMPORTED PREVIEWS"; + break; + case "cart": + title = "MY CART"; + break; + case "freebies": + title = "50 FREE MEDIA CLIPS"; + break; + case "bins": + console.log("SubTopRowController selected bin name:", BinsModel.showBin.name), title = "COLLECTION: " + BinsModel.showBin.name; + break; + case "search": + title = 0 < SearchModel.query.length ? SearchModel.query.toUpperCase() : ""; + break; + default: + title = "" + } + $scope.obj.title = title, "search" == ViewStateModel.getState() ? $scope.obj.showDropdown = !0 : $scope.obj.showDropdown = !1, "cart" == ViewStateModel.getState() ? $scope.obj.showCreditsWrapper = !0 : $scope.obj.showCreditsWrapper = !1, $scope.showClearAll() + } + $scope.obj = { + showFilters: !1, + titleClass: "sub-top-row-title-no-filters", + showClearAll: !1, + showDropdown: !0, + showCreditsWrapper: !1, + credits: 0 + }, $scope.$root.$on("on cart total", function(event) { + $scope.obj.credits = CartModel.getCartTotal().creditsData.availableSum + }), $scope.cartModel = function() { + return CartModel.cartVO + }, $scope.$watch($scope.cartModel, function() { + $scope.showClearAll() + }), $scope.$root.$on("bin selected", function(event) { + onViewStateChange() + }), $scope.viewStateModelQuery = function() { + return SearchModel.query + }, $scope.$watch($scope.viewStateModelQuery, onViewStateChange), $scope.viewStateModel = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewStateModel, onViewStateChange), $scope.showClearAll = function() { + "cart" == ViewStateModel.getState() && 0 < CartModel.cartVO.items.length ? $scope.obj.showClearAll = !0 : $scope.obj.showClearAll = !1 + }, $scope.$root.$on("filters button clicked", function(event, state) { + $scope.obj.titleClass = state ? "sub-top-row-title-filters" : "sub-top-row-title-no-filters" + }), $scope.onClearCartClicked = function() { + if (0 != CartModel.cartVO.items.length) { + for (var ids = "", i = 0; i < CartModel.cartVO.items.length; i++) i < CartModel.cartVO.items.length ? ids += CartModel.cartVO.items[i].id + "," : ids += CartModel.cartVO.items[i].id; + $scope.$root.$emit("clear cart requested", [ids]) + } + }, $scope.buyCreditsClicked = function() { + var ga = { + ec: "buy%20credits" + }; + console.log("CreditsController ga", ga), AnalyticsService.sendData(ga), $scope.$root.$emit("modal buy credits requested"), console.log("SubTopRowController button clicked") + } +}), app.controller("TileListItemController", function($scope, Service, BinsModel, ImportedPreviewsService, ViewStateModel, LoginModel, ReplaceModel, DownloadModel) { + $scope.childObj = {}, $scope.childObj.addedToCart = !1, $scope.childObj.addedToBin = !1, $scope.allowDownload = !0, $scope.childObj.cartClicked = !1, $scope.childObj.binClicked = !1, $scope.childObj.showEditorial = !0, $scope.childObj.viewState = "search", $scope.childObj.notification = "", "FCPX" === HOST_NAME ? $scope.childObj.importTooltip = "CLICK TO DOWNLOAD" : $scope.childObj.importTooltip = "CLICK TO IMPORT", $scope.viewState = function() { + return ViewStateModel.getState() + }, $scope.$watch($scope.viewState, function() { + $scope.childObj.viewState = ViewStateModel.getState() + }, !0), $scope.$root.$on("added to cart", function(event) { + $scope.childObj.cartClicked && ($scope.childObj.addedToCart = !0), setTimeout(function() { + $scope.childObj.cartClicked = !1, $scope.childObj.addedToCart = !1 + }, 1e3) + }), $scope.$root.$on("added to bin", function(event) { + $scope.childObj.binClicked && ($scope.childObj.addedToBin = !0), setTimeout(function() { + $scope.childObj.binClicked = !1, $scope.childObj.addedToBin = !1 + }, 1e3) + }), $scope.itemHovered = function(e) { + $scope.childObj.showMenu = !0, $scope.$root.$emit("start preview", $scope.item, e.clientX) + }, $scope.itemLeft = function() { + $scope.childObj.showMenu = !1, $scope.$root.$emit("stop preview", $scope.item) + }, $scope.opaqueClicked = function() { + console.log("TileListItemController opaqueClicked", $scope.allowDownload), $scope.allowDownload && ($scope.allowDownload = !1, $scope.$root.$emit("download requested", [$scope.item]), ImportedPreviewsService.saveItem($scope.item.id), $scope.$root.$emit("stop preview", $scope.item)), setTimeout(function() { + $scope.allowDownload = !0 + }, 2e3) + }, $scope.overInfoIcon = function() { + $scope.$root.$emit("preview info icon over") + }, $scope.outInfoIcon = function() { + $scope.$root.$emit("preview info icon out") + }, $scope.binIconClicked = function() { + console.log("TileListItemController binIconClicked"), LoginModel.loggedIn ? 0 < BinsModel.bins.length ? (console.log("TileListItemController binIconClicked show notification"), Service.modifyBin(BinsModel.addToBin.id, $scope.item.id), $scope.childObj.notification = "Added to the collection!", $scope.childObj.binClicked = !0, setTimeout(function() { + $scope.childObj.binClicked = !1, $scope.childObj.addedToBin = !1 + }, 4e3), $scope.childObj.binClicked = !0) : $scope.$root.$emit("modal simple requested", ["You don't have Collections", "In order to add clips to a Collection you first need to create a Collection"]) : $scope.$root.$emit("modal not logged in", [ERROR]) + }, $scope.cartIconClicked = function() { + $scope.childObj.notification = "Added to the cart successfully!", $scope.childObj.cartClicked = !0, setTimeout(function() { + $scope.childObj.cartClicked = !1, $scope.childObj.addedToCart = !1 + }, 4e3), Service.getFormats($scope.item) + }, $scope.trashIconClicked = function() { + $scope.$root.$emit("stop preview", $scope.item), "bins" === ViewStateModel.getState() ? Service.modifyBin(BinsModel.binVO.id, "", $scope.item.id) : "previews" === ViewStateModel.getState() && ImportedPreviewsService.deleteItem($scope.item.id) + }, $scope.linkClicked = function() { + opn("https://www.pond5.com/item/" + $scope.item.id) + } +}), app.controller("TileListSearchController", function($scope, SearchModel, Service) { + $scope.obj = { + showDeleteIcon: !1 + }, $scope.searchItems = function() { + if (SearchModel.searchResultVO) return SearchModel.searchResultVO.items + }, $scope.$watch($scope.searchItems, function() { + SearchModel.searchResultVO && ($scope.obj.items = SearchModel.searchResultItems) + }) +}), app.controller("TileListPreviewsController", function($scope, PreviewsModel) { + $scope.obj = { + showDeleteIcon: !0 + }, $scope.previewItems = function() { + if (PreviewsModel.previewsVO) return PreviewsModel.previewsVO.items + }, $scope.$watch($scope.previewItems, function() { + if (PreviewsModel.previewsVO) { + console.log("TileListPreviewsController: ", PreviewsModel.previewsVO), PreviewsModel.previewsVO.items.reverse(); + for (var previews = PreviewsModel.previewsVO.items, nonAEpreviews = [], i = 0; i < previews.length; i++) "AE" != previews[i].type && nonAEpreviews.push(previews[i]); + $scope.obj.items = nonAEpreviews + } + }) +}), app.controller("TileListBinsController", function($scope, BinsModel) { + $scope.obj = { + showDeleteIcon: !0 + }, $scope.binItems = function() { + if (BinsModel.binVO) return BinsModel.getBinVO() + }, $scope.$watch($scope.binItems, function() { + BinsModel.binVO && ($scope.obj.items = BinsModel.binVO.items) + }, !0) +}), app.controller("TileListFreebiesController", function($scope, FreebiesModel) { + $scope.obj = { + showDeleteIcon: !1 + }, $scope.freeItems = function() { + if (FreebiesModel.freebiesVO) return FreebiesModel.freebiesVO.items + }, $scope.$watch($scope.freeItems, function() { + FreebiesModel.freebiesVO && ($scope.obj.items = FreebiesModel.freebiesVO.items) + }) +}), app.controller("TransactionController", function($scope, ViewStateModel, ViewStateService, Service, AnalyticsService, CheckOutModel, ReplaceModel) { + $scope.obj = { + url: "", + show: !1 + }, $scope.CheckOutModel = function() { + return CheckOutModel + }, $scope.$watch($scope.CheckOutModel, function() { + if (CheckOutModel.checkOutURL) { + (new Date).getTime(); + $scope.obj.url = CheckOutModel.checkOutURL, $scope.obj.show = !0, CheckOutModel.checkOutURL = "", $("body,html").css("overflow", "hidden") + } + }, !0), window.parent.addEventListener("message", function() { + switch (ViewStateModel.allowPreviews = !0, console.log("TransactionController postMessage: ", event.data), event.data) { + case "PAID": + ReplaceModel.getState() === NOT_PURCHASED ? Service.getPurchases() : ($scope.$root.$emit("modal simple requested", PURCHASE_SUCCESSFULL), ViewStateService.viewRequested("downloads")), $scope.$root.$emit("purchase complete"), Service.getUserInfo(), console.log("TransactionController CC payment success"); + break; + case "CANCELED": + $scope.$root.$emit("modal simple requested", PURCHASE_CANCELED); + break; + default: + $scope.$root.$emit("modal simple requested", [ERROR, "UNKNOWN"]) + } + $scope.obj.show = !1, console.log("TransactionController onDone, show:", $scope.obj.show), $scope.$root.$emit("checkout complete"), $("body,html").css("overflow", "visible") + }, !1) +}), app.directive("enter", function() { + return function(scope, element, attrs) { + element.bind("keydown", function() { + 13 === event.which && scope.$apply(attrs.enter) + }) + } +}), app.directive("enterFooter", function() { + return function(scope, element, attrs) { + element.bind("mouseenter", function() { + element.children()[0].style.color = "#ccc" + }) + } +}), app.directive("leaveFooter", function() { + return function(scope, element, attrs) { + element.bind("mouseleave", function() { + element.children()[0].style.color = "#969493" + }) + } +}), app.directive("repositionImage", function() { + return { + restrict: "A", + link: function(scope, elem, attrs) { + elem.on("load", function() { + 108 < $(this).height() && elem.addClass("high") + }) + } + } +}), app.directive("rotate", function() { + return { + restrict: "A", + link: function(scope, element, attrs) { + scope.$watch(attrs.rotate, function(dir) { + var r = "rotate(" + ("up" === dir ? 180 : 0) + "deg)"; + element.css({ + "-webkit-transform": r + }) + }) + } + } +}), app.directive("whenScrolled", ["$window", "ScrollService", function($window, ScrollService) { + return function(scope, elm, attr) { + elm[0]; + angular.element($window).bind("scroll", function() { + ScrollService.onScroll() + }) + } +}]), app.directive("scrollTop", [function() { + return { + restrict: "A", + link: function(scope, $elm, attr) { + scope.$root.$on("scroll progress to top", function() { + $elm.animate({ + scrollTop: 0 + }, "slow") + }) + } + } +}]), app.directive("dragMe", function() { + return { + restrict: "A", + link: function(scope, elem, attr, ctrl) { + elem.draggable() + } + } +}), app.directive("onHoverInfoCart", function() { + return { + link: function(scope, element, attrs) { + element.bind("mouseenter", function($event) { + initialMouseX = $event.clientX, initialMouseY = $event.clientY, scope.$root.$emit("cart icon over", initialMouseX, initialMouseY) + }), element.bind("mouseleave", function() { + scope.$root.$emit("cart icon out") + }) + } + } +}), app.directive("onHoverPreview", function() { + return { + link: function(scope, element, attrs) { + element.bind("mouseenter", function($event) { + var previewX, previewY, tileX = element[0].getBoundingClientRect().left; + previewX = tileX < 310 ? tileX + 220 : tileX - 400, (previewY = element[0].getBoundingClientRect().top - 200) < 20 && (previewY = 20), 340 < previewY && (previewY = 340); + var cols = document.getElementsByClassName("preview"); + for (i = 0; i < cols.length; i++) cols[i].style.left = previewX.toString() + "px", cols[i].style.top = previewY.toString() + "px" + }) + } + } +}), app.filter("to_trusted", ["$sce", function($sce) { + return function(text) { + return $sce.trustAsHtml(text) + } +}]), app.filter("trusted", ["$sce", function($sce) { + return function(url) { + return $sce.trustAsResourceUrl(url) + } +}]), app.filter("secondsToDateTime", [function() { + return function(seconds) { + return new Date(1970, 0, 1).setSeconds(seconds) + } +}]), app.directive("closeCollectionsList", function($document) { + return { + restrict: "A", + link: function(scope, elem, attr, ctrl) { + elem.bind("click", function(e) { + e.stopPropagation() + }), $document.bind("click", function() { + scope.$apply(attr.closeCollectionsList) + }) + } + } +}), app.directive("fieldValidation", function() { + return { + require: "ngModel", + link: function(scope, element, attr, mCtrl) { + mCtrl.$parsers.push(function(value) { + return /^\w+$/.test(value) && 1 < value.toString().length || 0 == value.toString().length ? (mCtrl.$setValidity("charE", !0), console.log("directive valid true")) : (mCtrl.$setValidity("charE", !1), console.log("directive valid false")), value + }) + } + } +}), app.directive("vatValidation", function() { + return { + require: "ngModel", + link: function(scope, element, attr, mCtrl) { + mCtrl.$parsers.push(function(value) { + return /^\w+$/.test(value) && 2 < value.toString().length || 0 == value.toString().length ? (mCtrl.$setValidity("charE", !0), console.log("directive valid true")) : (mCtrl.$setValidity("charE", !1), console.log("directive valid false")), value + }) + } + } +}), app.directive("restrictInput", [function() { + return { + restrict: "A", + link: function(scope, element, attrs) { + var ele = element[0], + regex = RegExp(attrs.restrictInput), + value = ele.value; + ele.addEventListener("keyup", function(e) { + regex.test(ele.value) ? value = ele.value : ele.value = value + }) + } + } +}]), app.filter("searchFilter", function() { + return function(input, param1) { + if (console.log("------------------------------------------------- begin dump of custom parameters"), console.log("searchFilter input: ", input), input && input.length) { + console.log("searchFilter param1: ", param1); + var filteredItems = []; + for (i = 0; i < input.length; i++) input[i].fps == param1 && filteredItems.push(input[i]); + return filteredItems + } + } +}), PURCHASE_SUCCESSFULL = ["Your purchase has been successfull!", "Your items are now ready to download."], PURCHASE_CANCELED = ["Canceled.", "Purchase was canceled."], ERROR = "Oops, something went wrong...", NO_RESULTS = ["Your search returned no results", "
    • Try adjusting your filters
    • Check your search term for misspelling or try a few synonyms
    "], BM_VIDEO = 15, BM_MUSIC = 16, BM_SFX = 32, BM_PHOTO = 128, BM_ILLUSTRATIONS = 1024, BM_AFTER_EFFECTS = 64, BM_PUBLIC_DOMAIN = 16384, MODE = "live", THIRD_PARTY = "", TARGET_APP = "", GA_TRACKING_CODE = "UA-60083218-9", DEFAULT = "not replacing", MISSING_ITEMS = "missing items", NOT_PURCHASED = "not purchased", NOT_DOWNLOADED = "not downloaded", PURCHASED_AND_DOWNLOADED = "purchased and downloaded"; +var BASE_URL = "https://plugin.pond5.com/", + NO_RESULTS_ICON = BASE_URL + "pond5_shared/images/no_results_icon.png", + DRAGNDROP_IMG = BASE_URL + "pond5_shared/images/intro-icons/dragndrop.png", + STATE_IMG = BASE_URL + "pond5_shared/images/intro-states/step", + STATE_FCP_IMG = BASE_URL + "pond5_shared/images/intro-states-fcp/step", + DOWNLOAD_IMG = BASE_URL + "pond5_shared/images/intro-icons/download.png", + CART_IMG = BASE_URL + "pond5_shared/images/intro-icons/cart.png", + PREVIEWS_IMG = BASE_URL + "pond5_shared/images/intro-icons/previews.png", + DUMMY_IMG = BASE_URL + "pond5_shared/images/intro-icons/dummy.png", + CLEAR_CART_TRASH_IMG = BASE_URL + "pond5_shared/images/clear-cart-trash-icon.png", + CART_BUTTON_IMG = BASE_URL + "pond5_shared/images/cartButtonIcon.png", + PROGRESS_CLOSE_IMG = BASE_URL + "pond5_shared/images/progress-close-icon.png", + LOGO_IMG = BASE_URL + "pond5_shared/images/logo-white.png", + MODAL_SIMPLE_HTML = BASE_URL + "pond5_shared/views/modals/modalSimple.html", + MODAL_ADD_DESTINATION_HTML = BASE_URL + "pond5_shared/views/modals/modalAddDestination.html", + MODAL_ADD_COLLECTION_HTML = BASE_URL + "pond5_shared/views/modals/modalAddCollection.html", + MODAL_ADD_COLLECTION_CONFIRMATION_HTML = BASE_URL + "pond5_shared/views/modals/modalAddCollectionConfirmation.html", + MODAL_SELECT_SEQUENCES_HTML = BASE_URL + "pond5_shared/views/modals/modalSelectSequences.html", + MODAL_INTRO_HTML = BASE_URL + "pond5_shared/views/modals/modalIntro.html", + MODAL_ADD_TO_CART_HTML = BASE_URL + "pond5_shared/views/modals/modalAddToCart.html", + MODAL_BILLING_ADDRESS_HTML = BASE_URL + "pond5_shared/views/modals/modalBillingAddress.html", + MODAL_CHOOSE_BILLING_INFO_HTML = BASE_URL + "pond5_shared/views/modals/modalChooseBillingInfo.html", + MODAL_CHOOSE_FORMAT_HTML = BASE_URL + "pond5_shared/views/modals/modalChooseFormat.html", + MODAL_CHOOSE_VERSION_HTML = BASE_URL + "pond5_shared/views/modals/modalChooseVersion.html", + MODAL_FREEBIES_HTML = BASE_URL + "pond5_shared/views/modals/modalFreebies.html", + MODAL_LOGIN_HTML = BASE_URL + "pond5_shared/views/modals/modalLogin.html", + MODAL_NOT_LOGGED_IN_HTML = BASE_URL + "pond5_shared/views/modals/modalNotLoggedIn.html", + MODAL_PROMO_CODE_HTML = BASE_URL + "pond5_shared/views/modals/modalPromoCode.html", + MODAL_REMOVE_COLLECTION_HTML = BASE_URL + "pond5_shared/views/modals/modalRemoveCollection.html", + MODAL_REPLACE_HTML = BASE_URL + "pond5_shared/views/modals/modalReplace.html", + MODAL_REPLACE_WARNING_HTML = BASE_URL + "pond5_shared/views/modals/modalReplaceWarning.html", + MODAL_BUY_CREDITS_HTML = BASE_URL + "pond5_shared/views/modals/modalBuyCredits.html", + COLLECTIONS_LIST_HTML = BASE_URL + "pond5_shared/views/collectionsList.html"; +$(function() { + Offline.options = { + checkOnLoad: !0, + checks: { + image: { + url: function() { + return "https://plugin.pond5.com/pond5_shared/images/logo-white.png?_=" + Math.floor(1e9 * Math.random()) + } + }, + active: "image" + } + } +}), app.service("AppModel", ["$rootScope", function($rootScope) { + var path = require("path"), + dirHomePond5 = getUserHome() + path.sep + "pond5", + dirImports = dirHomePond5 + path.sep + "imports", + dirPrefs = dirHomePond5 + path.sep + "prefs", + dirDestinations = dirHomePond5 + path.sep + "destinations", + dirDefaultLib = path.sep, + dirUser = dirHomePond5 + path.sep + "user", + result = (dirDefaultLib = dirHomePond5 + path.sep + "defaultLib", { + OS: "", + baseFolders: [], + currentBaseFolder: "", + previewsDir: "", + purchasedDir: "", + defaultLib: "", + defaultLibName: "", + defaultLibPath: "", + targetApp: "", + setEnv: function() { + result.setOS(os.platform()), $rootScope.$emit("environment set") + }, + getOS: function() { + return result.OS + }, + setOS: function(s) { + result.OS = s + }, + getDocumentsPath: function() { + return os.homedir() + path.sep + "Documents" + }, + getDirHomePond5: function() { + return dirHomePond5 + }, + getDirImports: function() { + return dirImports + }, + getDirDestinations: function() { + return dirDestinations + }, + getDirPrefs: function() { + return dirPrefs + }, + getDirUser: function() { + return dirUser + }, + getDestinationsXML: function() { + return result.getDirDestinations() + path.sep + "destinations.xml" + }, + getUserXML: function() { + return result.getDirUser() + path.sep + "user.xml" + }, + getPreferencesXML: function() { + return result.getDirPrefs() + path.sep + "preferences.xml" + }, + getDirDefaultLib: function() { + return dirDefaultLib + }, + getDefaultLib: function() { + return result.defaultLib + }, + setDefaultLib: function(path) { + "/" == path.substr(path.length - 1) && (path = path.slice(0, -1)), result.setDefaultLibName(path), result.setDefaultLibPath(path), result.defaultLib = path + }, + getDefaultLibName: function() { + return result.defaultLibName + }, + setDefaultLibName: function(path) { + var n = path.lastIndexOf("/"); + result.defaultLibName = path.substring(n + 1).replace(".fcpbundle", "") + }, + getDefaultLibPath: function() { + return result.defaultLibPath + }, + setDefaultLibPath: function(path) { + result.defaultLibPath = path.substring(0, path.lastIndexOf("/")) + }, + getDefaultLibXML: function() { + return result.getDirDefaultLib() + path.sep + "defaultLib.xml" + }, + getTargetApp: function() { + return result.targetApp + }, + setTargetApp: function(app) { + result.targetApp = app + } + }); + return result +}]), app.factory("BillingInfoModel", ["$rootScope", function($rootScope) { + var info = { + onBillingInfo: function(data) { + info.setBillingInfo(data.commands[0]), info.getBillingInfo().forEach(function(item) { + item.isdefault && info.setDefaultInfo(item) + }) + }, + setBillingInfo: function(data) { + info.billingInfo = data + }, + getBillingInfo: function() { + return info.billingInfo + }, + setDefaultInfo: function(data) { + info.defaultInfo = data + }, + getDefaultInfo: function() { + return info.defaultInfo + } + }; + return info +}]), app.service("BinsModel", ["$rootScope", function($rootScope) { + var result = { + binsVO: null, + bins: [], + binVO: null, + showBin: null, + addToBin: null, + onBins: function(data) { + result.binsVO = new BinsVO(data.commands[0]), result.bins = result.binsVO.bins, $rootScope.$emit("onBins") + }, + onBin: function(data) { + result.setBinVO(new BinVO(data.commands[0])) + }, + onActiveBin: function(data) { + result.bins.forEach(function(bin) { + bin.id == data.commands[0].binid && (result.addToBin = bin) + }), $rootScope.$emit("active bin changed", result.addToBin) + }, + setBinVO: function(data) { + result.binVO = data + }, + getBinVO: function() { + return result.binVO + } + }; + return result +}]); +var BinsVO = function BinsVO(data) { + var i; + for (this.bins = [], i = 0; i < data.bins.length; i += 1) { + var bin = {}; + bin.name = data.bins[i].name, bin.abbrBinName = getAbbrName(bin.name, 17), bin.id = data.bins[i].id, bin.total = data.bins[i].tot, bin.selected = !1, this.bins[i] = bin + } + this.bins.sort(compare), BinsVO.prototype = { + toString: function() { + console.log("bins: " + this.bins) + } + } + }, + BinVO = function BinVO(data) { + var itemVO, i; + this.items = [], this.id = data.binid, this.name = data.name, this.jpegBase = "http://ec.pond5.com/s3/", console.log("BinVO id: ", data.binid, data.name); + var filterVS = 0; + for (filterVS = "AEFT" == HOST_NAME ? 200 : 102, i = 0; i < data.items.length; i += 1) parseInt(data.items[i].vs) <= filterVS && (itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base, "", this.jpegBase), this.items.push(itemVO)); + BinVO.prototype = { + toString: function() { + console.log("name & id: ", this.id, this.name) + } + } + }; +app.factory("CartModel", ["$rootScope", "ReplaceModel", function($rootScope, ReplaceModel) { + $rootScope.$on("on cart", function(event, data) { + result.onCart(data) + }), $rootScope.$on("on cart total", function(event, data) { + result.onCartTotal(data) + }), $rootScope.$on("formats complete", function(event, item, formats) { + console.log("CartModel onCart ReplaceModel.getState(): ", ReplaceModel.getState()), result.onFormats(item, formats) + }); + var result = { + cartVO: [], + cartTotal: null, + onCart: function(data) { + result.cartVO = new ItemsVO(data.commands[0]) + }, + onCartTotal: function(data) { + result.setCartTotal(data.commands[0]) + }, + onFormats: function(item, formats) { + if (console.log("CartModel onFormats, num of formats for id: ", item, formats.length), 1 < formats.length) { + var uniqueResFormats = _.uniq(formats, function(p) { + return p.ti + }); + $rootScope.$emit("on add to cart clicked", uniqueResFormats) + } else { + var apiObj = { + fn: "modifyCart", + args: [item.id, ""] + }; + $rootScope.$emit("api call", apiObj) + } + }, + setCartTotal: function(data) { + result.cartTotal = data + }, + getCartTotal: function() { + return result.cartTotal + } + }; + return result +}]), app.factory("CheckOutModel", ["$sce", function($sce) { + var result = { + onPurchase: function(data) { + console.log("CheckOutModel onPurchase, url: ", data.commands[0].url); + (new Date).getTime(); + result.checkOutURL = $sce.trustAsResourceUrl(data.commands[0].url), console.log("CheckOutModel onPurchase, url: ", result.checkOutURL) + } + }; + return result +}]), app.factory("DownloadModel", ["$rootScope", "PurchasesModel", "ReplaceModel", function($rootScope, PurchasesModel, ReplaceModel) { + var result = { + binBatch: null, + itemsDownloadList: [], + selectedVersion: 0, + downloadingBatchURLs: !1, + urlCounter: 0, + downloadCounter: -1, + stayAwake: !1, + onGetPurchaseURL: function(data) { + var item = result.getVersionByID(data.commands[0].bid); + item && (item.hiresURL = data.commands[0].url, item.downloadType = "purchase", "AE" == item.vs && (item.type = item.vs), $rootScope.$emit("download requested", [item])) + }, + onGetAllPurchaseURLs: function(data) { + var i, purchase, purchases = []; + for (ReplaceModel.getState() === DEFAULT ? purchases = PurchasesModel.purchasesVO.items : ReplaceModel.getState() === NOT_DOWNLOADED && (purchases = ReplaceModel.missingDownloads), result.urlCounter++, i = 0; i < purchases.length; i += 1) { + purchase = purchases[i]; + var dataItem = data.commands[0]; + for (k = 0; k < purchase.formats.length; k += 1) purchase.formats[k].id == dataItem.bid && (purchase.hiresURL = dataItem.url, purchase.downloadType = "purchase"); + purchase.id == dataItem.bid && (purchase.hiresURL = dataItem.url, purchase.downloadType = "purchase", purchase.versions && 0 < purchase.versions.length && (purchase.vs = purchase.versions[0].vs)) + } + purchases = purchases.filter(function(v, i, a) { + return a.indexOf(v) == i + }), result.urlCounter === purchases.length && ($rootScope.$emit("download requested", purchases), result.urlCounter = 0, result.downloadingBatchURLs = !1) + }, + getVersionByID: function(id) { + var foundItem; + if (PurchasesModel.purchasesVO.items.forEach(function(item) { + item.id === id && (item.parentFormatID && (item.versions[result.selectedVersion].parentFormatID = item.parentFormatID), foundItem = item.versions[result.selectedVersion]) + }), foundItem) return foundItem + } + }; + return result +}]), app.factory("FreebiesModel", [function() { + var result = { + onFreebies: function(data) { + result.freebiesVO = new ItemsVO(data.commands[0]) + } + }; + return result +}]); +var HiresVO = function HiresVO(dest, name) { + this.dest = dest, this.name = name, this.path = dest + name, this.id = name.split(" ")[1], this.replace = !1, this.type = "", this.nameFCP = this.name.replaceAll(" ", "%20"), this.nameFCP = this.nameFCP.replaceAll("-", "%2D"), this.nameFCP = this.nameFCP.replaceAll("&", "and"), this.pathFCP = "file://" + this.path.replaceAll(" ", "%20"), this.pathFCP = this.pathFCP.replaceAll("-", "%2D"), this.pathFCP = this.pathFCP.replaceAll("&", "and"), HiresVO.prototype = { + toString: function() { + return "\nHiresVO path: " + this.path + "\nname: " + this.name + "\nid: " + this.id + "\nreplace: " + this.replace + } + } + }, + ItemsVO = function ItemsVO(data) { + var itemVO, i; + for (this.tot_nbr_rows = data.tot_nbr_rows, this.max_per_page = data.max_per_page, this.nbr_footage = data.nbr_footage, this.nbr_music = data.nbr_music, this.nbr_sfx = data.nbr_sfx, this.nbr_total = data.nbr_total, this.items = [], i = 0; i < data.items.length; i += 1) itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base, ""), this.items[i] = itemVO; + ItemsVO.prototype = { + toString: function() { + console.log("vs: " + this.vs) + } + } + }, + ItemVO = function ItemVO(data, iconBase, flvBase, parentID) { + var getURL; + this.selectedVersion = 0, this.name = data.n, this.abbrName = getAbbrName(this.name, 25), this.abbrTileName = getAbbrName(this.name, 22), this.abbrListName = getAbbrName(this.name, 40), this.artistName = getAbbrName(data.artistname, 40), this.id = data.id, this.title = data.ti, this.vr360 = data.vr360, data.pr < .001 ? this.price = "0" : this.price = data.pr, this.priceRange = data.pricerange, this.vs = getConvertedVideoStandard(data.vs), this.downloadType = "preview", this.downloadURL, this.downloadDestination = "", this.downloading = !1, this.progressPerc = "", this.progressMB = "", this.progressName = "", this.parentFormatID = "", this.canceled = !1, this.completed = !1, this.imported = !1, this.inCart = !1, this.inDownloads = !1, this.selected = !1, this.formats = [], this.versions = [], this.ox = data.ox, this.oy = data.oy, this.ar = getAspectRatio(data.ar), this.ar || (this.ar = "n/a"), this.aq = data.aq, this.dur = data.dur, data.fps ? this.fps = data.fps : this.fps = "n/a", data.ti && (this.title = data.ti), data.tb && (this.subTitle = data.tb), data.i && (this.additionalInfo = data.i), data.id ? this.id = data.id : this.id = parentID, 0 === this.id.length && (this.id = parentID), this.offset = data.so, this.transactionID = data.tr, this.expirationDate = data.exp, this.versionID = data.v, this.videoCodec = data.codg, this.audioCodec = data.coda, this.extension = data.ext, this.version = data.bitoffset, this.type = getMediaType(this.vs), this.baseURL = flvBase || "https://api-cdn.pond5.com/", getURL = function(id, type, baseURL) { + var url; + switch (type) { + case "icon": + url = iconBase + ExtendedID.extend(id) + "_iconv.jpeg"; + break; + case "H264": + url = baseURL + ExtendedID.extend(id) + "_main_xl.mp4"; + break; + case "vr360": + url = baseURL + ExtendedID.extend(id) + "_main360.mp4"; + break; + case "mov": + url = baseURL + ExtendedID.extend(id) + "_prev_264.mov"; + break; + case "flv": + url = baseURL + ExtendedID.extend(id) + "_prev_xl.flv"; + break; + case "mp3": + url = baseURL + ExtendedID.extend(id) + "_prev.mp3"; + break; + case "m4a": + url = baseURL + ExtendedID.extend(id) + "_prev.m4a"; + break; + case "icon large": + url = iconBase + ExtendedID.extend(id) + "_iconl.jpeg" + } + return url + }, this.iconURL = getURL(this.id, "icon", this.baseURL), this.iconLargeURL = getURL(this.id, "icon large", this.baseURL), this.vr360 ? this.h264URL = getURL(this.id, "vr360", this.baseURL) : this.h264URL = getURL(this.id, "H264", this.baseURL), this.mp3URL = getURL(this.id, "mp3", this.baseURL), this.m4aURL = getURL(this.id, "m4a", this.baseURL), ItemVO.prototype = {} + }; +app.factory("LoginModel", [function() { + var data = { + getLoggedIn: function() { + return data.loggedIn + }, + setLoggedIn: function(state) { + data.loggedIn = state + }, + getCX: function() { + return data.cx + }, + setCX: function(cx) { + data.cx = cx + }, + getCM: function() { + return data.cm + }, + setCM: function(cm) { + data.cm = cm + } + }; + return data +}]), app.service("MissingItemsModel", [function() { + return { + missingItemsVO: null + } +}]); +var MissingItemsVO = function MissingItemsVO(data) { + var i; + for (this.items = [], i = 0; i < data.items.length; i += 1) this.itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base), this.items[i] = this.itemVO; + MissingItemsVO.prototype = {} +}; +app.factory("PreviewsModel", [function() { + var result = { + onPreviews: function(data) { + console.log("PreviewsModel onPreviews: ", data), result.previewsVO = new ItemsVO(data.commands[0]) + } + }; + return result +}]); +var PreviewVO = function PreviewVO(dest, path) { + var parts = (this.path = path).split("/"); + this.name = parts[parts.length - 1], this.id = this.name.split(" ")[0], PreviewVO.prototype = { + toString: function() { + return "\nPreviewVO path: " + this.path + "\nname: " + this.name + "\nid: " + this.id + } + } +}; +app.service("PurchasesModel", ["$rootScope", "AnalyticsService", function($rootScope, AnalyticsService) { + $rootScope.$on("on purchases", function(event, data) { + result.onGetPurchases(data) + }), $rootScope.$on("purchase complete", function(event) { + console.log("PurchasesModel purchase complete handler"), result.sendGA = !0 + }); + var result = { + purchasesVO: [], + sendGA: !1, + onGetPurchases: function(data) { + result.purchasesVO = new PurchaseVO(data.commands[0]), $rootScope.$emit("on purchases vo", result.purchasesVO), console.log("PurchasesModel onGetPurchases result.purchasesVO: ", result.purchasesVO), result.sendGA && (AnalyticsService.sendData(result.purchasesVO, "transaction"), result.sendGA = !1) + } + }; + return result +}]); +var PurchaseVO = function PurchaseVO(data) { + var i; + this.items = []; + for ("AEFT" == HOST_NAME ? 200 : 102, i = 0; i < data.items.length; i += 1) { + var j; + for (this.itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base, data.items[i].bid), this.itemVO.transactionID = data.items[i].versions[0].tr, this.itemVO.name = data.items[i].versions[0].n, this.itemVO.abbrName = getAbbrName(this.itemVO.name, 30), this.itemVO.expirationDate = data.items[i].versions[0].exp, this.itemVO.parentFormatID = data.items[i].versions[0].vm, this.itemVO.type = getMediaType(getConvertedVideoStandard(data.items[i].versions[0].vs)), this.itemVO.aq = data.items[i].versions[0].aq, this.itemVO.versionID = data.items[i].versions[0].v, this.itemVO.version = data.items[i].versions[0].bitoffset, j = 0; j < data.items[i].versions.length; j += 1) this.itemVO.versions[j] = new ItemVO(data.items[i].versions[j], data.icon_base, data.flv_base, data.items[i].bid); + this.items.push(this.itemVO) + } + PurchaseVO.prototype = { + toString: function() { + console.log("name & id: ", this.items) + } + } +}; + +function checkNested(obj) { + for (var args = Array.prototype.slice.call(arguments), i = (obj = args.shift(), 0); i < args.length; i++) { + if (!obj.hasOwnProperty(args[i])) return !1; + obj = obj[args[i]] + } + return !0 +} + +function compare(a, b) { + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 +} + +function sortArgs() { + return Array.prototype.slice.call(arguments, 0).sort()[0] +} + +function getAspectRatio(as) { + var standard; + switch (as) { + case 1: + standard = "4:3"; + break; + case 2: + standard = "16:9 anamorphic"; + break; + case 3: + standard = "16:9 letterboxed"; + break; + case 4: + standard = "n/a"; + break; + case 5: + standard = "Other"; + break; + case 6: + standard = "16:9 native" + } + return standard +} + +function convertAspectRatio($max_x, $max_y, $aspect_quotient) { + var $out_x, $out_y; + return $aspect_quotient ? ($out_y = $max_y, $max_x < ($out_x = Math.round($max_y * parseFloat($aspect_quotient))) && ($out_x = $max_x, $out_y = Math.round($max_x / parseFloat($aspect_quotient))), new Point($out_x, $out_y)) : ($out_x = $max_x, $out_y = $max_y, new Point(370, 208)) +} +app.factory("ReplaceModel", ["$rootScope", function($rootScope) { + var result = { + clipsInSequences: [], + aeItemsinProjectView: [], + state: DEFAULT, + missingDownloads: [], + hiresOnFS: [], + previewsOnFS: [], + sequences: [], + setState: function(newState) { + result.state = newState, console.log("ReplaceModel STATE:", result.state), result.state === DEFAULT && $rootScope.$root.$emit("replacing complete") + }, + getState: function() { + return result.state + }, + getAEItems: function() { + return result.aeItemsinProjectView + }, + setAEItems: function(items) { + result.aeItemsinProjectView = items + }, + setSequenceNames: function(seqNames) { + result.sequences = []; + for (var i = 0; i < seqNames.length; i++) { + var obj = { + name: seqNames[i], + checked: !1 + }; + result.sequences[i] = obj + } + 0 < seqNames.length ? $rootScope.$root.$emit("modal select sequences", result.sequences) : ($rootScope.$root.$emit("modal simple requested", ["Replace With Hi-Res Clips - Warning", "The 'Replace With Hi-Res clips' button replaces lo-res previews with hi-res clips that you have purchased and downloaded.

    There are currently no sequences in your project."]), result.setState(DEFAULT)) + }, + setSequences: function(sequences) { + result.sequences = []; + for (var i = 0; i < sequences.length; i++) sequences[i].checked = !1; + var newArray = []; + newArray.push(sequences[0]); + for (i = 1; i < sequences.length; i++) { + for (var j = 0; j < newArray.length; j++) newArray[j].name === sequences[i].name && (console.log("already exists ", i, j, sequences[i].name), 0, sequences[i].name = sequences[i].name + " (id: " + sequences[i].id + ")"); + newArray.push(sequences[i]) + } + result.sequences = newArray, console.log("ReplaceModel, sequences:", result.sequences), 0 < sequences.length ? $rootScope.$root.$emit("modal select sequences", result.sequences) : ($rootScope.$root.$emit("modal simple requested", ["Replace With Hi-Res Clips - Warning", "The 'Replace With Hi-Res clips' button replaces lo-res previews with hi-res clips that you have purchased and downloaded.

    There are currently no sequences in your project."]), result.setState(DEFAULT)) + }, + setComps: function(comps) { + result.sequences = comps, $rootScope.$root.$emit("modal select comps", result.sequences) + }, + addHires: function(dest, files) { + for (var hiresVO, i = 0; i < files.length; i += 1)(hiresVO = new HiresVO(dest, files[i].fileName)).type = files[i].vs, hiresVO.replace = !0, result.hiresOnFS.push(hiresVO) + } + }; + return result +}]), app.service("SearchModel", ["$rootScope", function($rootScope) { + var result = { + allowInfiniteScroll: !1, + searchResultItems: [], + numOfResults: 0, + onSearch: function(data) { + result.searchResultVO = new ItemsVO(data.commands[0]), result.numOfResults = data.commands[0].nbr_footage + data.commands[0].nbr_music + data.commands[0].nbr_sfx + data.commands[0].nbr_ae, console.log("SearchModel onSearch num of results: ", result.numOfResults), "replace" === result.resultType && (result.searchResultItems = [], window.scrollTo(0, 0), 0 === result.numOfResults ? $rootScope.$emit("message view requested", !0, NO_RESULTS, !0, NO_RESULTS_ICON) : $rootScope.$emit("message view requested", !1)); + for (var i = 0; i < result.searchResultVO.items.length; i++) result.searchResultItems.push(result.searchResultVO.items[i]); + result.isSearching = !1, resizePanel() + }, + sumOfBitmasks: "", + query: "", + filter: "1", + resultType: "replace", + page: 0, + isSearching: !1, + filteredItems: [], + fps: "", + fpsgt: "", + res: "", + pricegt: "", + pricelt: "", + durationgt: "", + durationlt: "" + }; + return result +}]), app.factory("UserModel", [function() { + var firstTimeUser = !0, + user = { + onUserInfo: function(data) { + user.setCredits(data.credit), user.setUserName(data.un), user.setFirstName(data.fn), user.setLastName(data.ln), user.setAvatarURL(data.icon_base, data.av) + }, + setCredits: function(num) { + user.credits = num + }, + getCredits: function() { + return user.credits + }, + setUID: function(uid) { + user.uid = uid + }, + getUID: function() { + return user.uid + }, + setCM: function(cm) { + user.cm = cm + }, + getCM: function() { + return user.cm + }, + setCX: function(cx) { + user.cx = cx + }, + getCX: function() { + return user.cx + }, + setUserName: function(name) { + user.userName = name + }, + getUserName: function() { + return user.userName + }, + setFirstName: function(name) { + user.firstName = name + }, + getFirstName: function() { + return user.firstName + }, + setLastName: function(name) { + user.lastName = name + }, + getLastName: function() { + return user.lastName + }, + setAvatarURL: function(base, url) { + user.avatarURL = base + url + }, + getAvatarURL: function() { + return user.avatarURL + }, + setFirstTimeUser: function(state) { + firstTimeUser = state + }, + getFirstTimeUser: function() { + return firstTimeUser + } + }; + return user +}]), app.factory("VersionsModel", ["$rootScope", function($rootScope) { + var result = { + versions: [], + setVersions: function(v) { + result.versions = []; + for (var i = 0; i < v.length; i++) result.versions[i] = v[i]; + $rootScope.$emit("on versions selected", result.versions) + }, + getVersions: function() { + return result.versions + } + }; + return result +}]), app.factory("ViewStateModel", ["$rootScope", "SearchModel", function($rootScope, SearchModel) { + var state; + return { + allowPreviews: !1, + setState: function(s) { + state = s, SearchModel.allowInfiniteScroll = "search" === state || ($rootScope.$emit("filters button clicked", !1), !1) + }, + getState: function() { + return state + } + } +}]), app.service("AnalyticsService", ["$http", "$rootScope", "UserModel", "CartModel", function($http, $rootScope, UserModel, CartModel) { + var result = { + sendData: function(data, type) { + GA_TRACKING_CODE, + UserModel.getUID(), + UserModel.getUID(), + HOST_NAME, + PLUGIN_VERSION + }, + send: function(payload) { + $http({ + method: "POST", + url: payload + }).then(function(response) { + console.log("AnalyticsService then: ", response) + }, function(response) { + console.log("AnalyticsService error: ", response) + }) + } + }; + return result +}]), app.service("Service", ["$rootScope", "APIService", "LoginModel", "UserModel", "SearchModel", "FreebiesModel", "BinsModel", "ViewStateModel", "DownloadModel", "CheckOutModel", "PreviewsModel", "ReplaceModel", "ViewStateService", "ImportedPreviewsService", "AnalyticsService", "UserService", "BillingInfoModel", function($rootScope, APIService, LoginModel, UserModel, SearchModel, FreebiesModel, BinsModel, ViewStateModel, DownloadModel, CheckOutModel, PreviewsModel, ReplaceModel, ViewStateService, ImportedPreviewsService, AnalyticsService, UserService, BillingInfoModel) { + $rootScope.$on("api call", function(event, apiObj) { + call[apiObj.fn](sortArgs(apiObj.args)) + }); + var call = { + login: function() { + var obj = [{ + command: "login", + username: arguments[0][0], + password: arguments[0][1] + }]; + APIService.call(obj).then(function(data) { + LoginModel.setLoggedIn(!0), LoginModel.setCX(data.commands[0].cx), LoginModel.setCM(data.commands[0].cm), UserService.saveData(data.commands[0].cx, data.commands[0].cm), call.getUserInfo() + }).catch(function(err) {}) + }, + logout: function() { + console.log("Service logout"); + APIService.call([{ + command: "logout" + }]).then(function(data) { + LoginModel.setLoggedIn(!1) + }).catch(function(err) {}) + }, + getUserInfo: function() { + APIService.call([{ + command: "userinfo" + }]).then(function(data) { + "" != data.commands[0].uid && (UserModel.onUserInfo(data.commands[0]), call.getBins(), setTimeout(function() { + call.getCart() + }, 1e3), call.getActiveBin(), call.getBillingAddresses(), LoginModel.getLoggedIn() || LoginModel.setLoggedIn(!0)) + }).catch(function(err) {}) + }, + search: function() { + var obj = [{ + command: "search", + query: SearchModel.query + SearchModel.res + SearchModel.fps + SearchModel.fpsgt + SearchModel.pricegt + SearchModel.pricelt + SearchModel.durationgt + SearchModel.durationlt, + sb: SearchModel.filter, + bm: SearchModel.sumOfBitmasks, + no: "25", + p: SearchModel.page, + col: "1523" + }]; + APIService.call(obj).then(function(data) { + SearchModel.onSearch(data), ViewStateModel.allowPreviews = !0 + }).catch(function(err) {}) + }, + getFreeClips: function() { + APIService.call([{ + command: "get_free_clips" + }]).then(function(data) { + FreebiesModel.onFreebies(data) + }).catch(function(err) {}) + }, + getCart: function() { + APIService.call([{ + command: "get_cart_formatted", + artistinfo: "1" + }]).then(function(data) { + console.log("Service getCart data", data), $rootScope.$emit("on cart", data) + }).catch(function(err) {}) + }, + getCartTotal: function() { + var obj = [{ + command: "get_cart_total", + addressid: BillingInfoModel.getDefaultInfo() ? BillingInfoModel.getDefaultInfo().addressid : "", + use_credits: "1" + }]; + APIService.call(obj).then(function(data) { + $rootScope.$emit("on cart total", data) + }).catch(function(err) {}) + }, + getBillingAddresses: function(setState) { + APIService.call([{ + command: "get_billing_addresses" + }]).then(function(data) { + BillingInfoModel.onBillingInfo(data), setState && $rootScope.$emit("on modal choose billing info requested"), call.getCartTotal() + }).catch(function(err) {}) + }, + setBillingAddress: function(info) { + console.log("Service setBillingAddresses obj:", info); + var data = info[0]; + data.addressID || (data.addressID = ""); + var obj = [{ + command: "set_billing_address", + country: data.country, + addressid: data.addressID, + first_name: data.firstName, + last_name: data.lastName, + company_name: data.organization, + company_department: data.department, + company_id: data.companyID, + vat_id: data.vatID, + street1: data.street1, + street2: data.street2, + city: data.city, + state: data.state, + province: data.province, + postal_code: data.zipCode + }]; + APIService.call(obj).then(function(data) { + call.getBillingAddresses(!0) + }).catch(function(err) {}) + }, + getBins: function() { + APIService.call([{ + command: "get_bins" + }]).then(function(data) { + BinsModel.onBins(data) + }).catch(function(err) {}) + }, + getActiveBin: function() { + APIService.call([{ + command: "get_active_bin" + }]).then(function(data) { + BinsModel.onActiveBin(data) + }).catch(function(err) {}) + }, + setActiveBin: function(id) { + var obj = [{ + command: "set_active_bin", + binid: id + }]; + APIService.call(obj).then(function(data) { + setTimeout(function() { + call.getActiveBin() + }, 1e3) + }).catch(function(err) {}) + }, + getBin: function() { + var obj = [{ + command: "get_bin_formatted", + binid: BinsModel.showBin.id + }]; + APIService.call(obj).then(function(data) { + BinsModel.onBin(data) + }).catch(function(err) {}) + }, + modifyBin: function(binID, addID, rmID) { + var obj = [{ + command: "modify_active_bin", + binid: binID, + addid: addID, + rmid: rmID + }]; + APIService.call(obj).then(function(data) { + "1" == data.commands[0].nbr_removed ? call.getBin(BinsModel.binVO.id) : $rootScope.$emit("added to bin") + }).catch(function(err) {}) + }, + createBin: function(binName) { + var obj = [{ + command: "create_bin", + name: binName + }]; + APIService.call(obj).then(function(data) { + BinsModel.newBinName; + call.setActiveBin(data.commands[0].binid), call.getBins() + }).catch(function(err) {}) + }, + removeBin: function(id) { + var obj = [{ + command: "delete_bin", + binid: id + }]; + APIService.call(obj).then(function(data) { + call.getBins(), $rootScope.$emit("collection removed", data) + }).catch(function(err) {}) + }, + getPurchases: function() { + APIService.call([{ + command: "get_downloads_formatted" + }]).then(function(data) { + console.log("Service getPurchases data", data), $rootScope.$emit("on purchases", data) + }).catch(function(err) {}) + }, + getPurchaseURL: function(itemID, transactionID, versionID, version) { + console.log("Service getPurchaseURL", itemID, transactionID, versionID, version); + var obj = [{ + command: "download", + bid: itemID, + tr: transactionID, + v: versionID, + bitoffset: version + }]; + APIService.call(obj).then(function(data) { + console.log("Service getPurchaseURL data", data), DownloadModel.downloadingBatchURLs ? DownloadModel.onGetAllPurchaseURLs(data) : DownloadModel.onGetPurchaseURL(data) + }).catch(function(err) {}) + }, + modifyCart: function() { + var obj = [{ + command: "modify_active_cart", + addid: arguments[0][0], + rmid: arguments[0][1] + }]; + APIService.call(obj).then(function(data) { + 1 === data.commands[0].nbr_added && $rootScope.$emit("added to cart"), call.getCart(), call.getCartTotal() + }).catch(function(err) {}) + }, + purchaseWithCredits: function(buyAnyway, userData) { + var obj = [{ + command: "purchase_using_credits", + override: buyAnyway, + userdata: userData, + addressid: BillingInfoModel.getDefaultInfo().addressid + }]; + APIService.call(obj).then(function(data) { + console.log("purchaseWithCredits data", data), ReplaceModel.getState() === DEFAULT && $rootScope.$emit("modal simple requested", ["Your purchase has been successful!", "Your items are now ready to download."]), $rootScope.$emit("purchase complete"), ReplaceModel.getState() === NOT_PURCHASED ? call.getPurchases() : ViewStateService.viewRequested("downloads"), call.getUserInfo() + }).catch(function(err) {}) + }, + purchaseWithCash: function(buyAnyway, userData) { + var obj = [{ + command: "purchase_using_cash", + AdobePremierePlugin: "html", + override: buyAnyway, + userdata: userData, + addressid: BillingInfoModel.getDefaultInfo().addressid, + use_credits: "1" + }]; + APIService.call(obj).then(function(data) { + console.log("Service purchaseWithCash data", data), CheckOutModel.onPurchase(data) + }).catch(function(err) {}) + }, + promoRedeem: function(code) { + var obj = [{ + command: "promo_redeem", + promocode: code + }]; + APIService.call(obj).then(function(data) { + call.getUserInfo(), $rootScope.$emit("promo code added", data) + }).catch(function(err) {}) + }, + getImportedPreviews: function() { + console.log("Service getImportedPreviews", ImportedPreviewsService.idsString); + var obj = [{ + command: "get_clip_data_array", + itemids: ImportedPreviewsService.idsString, + col: "1523", + verboselvl: "100" + }]; + APIService.call(obj).then(function(data) { + PreviewsModel.onPreviews(data) + }).catch(function(err) {}) + }, + getFormats: function(item) { + console.log("Service getFormats", item.id); + var obj = [{ + command: "get_versions_formatted", + vm: item.id + }]; + APIService.call(obj).then(function(data) { + console.log("Service getFormats data", data); + var formats = data.commands[0].items; + $rootScope.$emit("formats complete", item, formats) + }).catch(function(err) {}) + }, + getFormatsReplacing: function(item) { + console.log("Service getFormatsReplacing", item.id); + var obj = [{ + command: "get_versions_formatted", + vm: item.id + }]; + APIService.call(obj).then(function(data) { + console.log("Service getFormatsReplacing data", data); + var formats = data.commands[0].items; + $rootScope.$emit("formats replacing complete", item, formats) + }).catch(function(err) {}) + }, + getMissingItems: function(itemIDsString) { + console.log("Service getMissingItems itemIDsString", itemIDsString); + var obj = [{ + command: "get_clip_data_array", + itemids: itemIDsString, + col: "1523", + verboselvl: "100" + }]; + APIService.call(obj).then(function(data) { + ReplaceModel.setState(MISSING_ITEMS), console.log("Service getMissingItems data", data), $rootScope.$emit("missing items complete", data) + }).catch(function(err) {}) + } + }; + return call +}]), app.factory("APIService", ["$http", "ViewStateModel", "LoginModel", function($http, ViewStateModel, LoginModel) { + return { + call: function(data) { + ViewStateModel.allowPreviews = !1; + var url, secret, apiKey, _0xf310 = ["test", "https://test.pond5.com/?page=api", "live", "https://www.pond5.com/?page=api", "oi23Jan3Inwh2io", "220655_769351580"]; + MODE === _0xf310[0] ? API_URL = _0xf310[1] : MODE === _0xf310[2] && (API_URL = _0xf310[3]), API_SECRET = _0xf310[4], API_KEY = _0xf310[5], url = API_URL, secret = API_SECRET, apiKey = API_KEY; + var stringified = JSON.stringify(data), + md5target = stringified + secret + "dragspel", + md5tostring = CryptoJS.MD5(md5target).toString(), + cx = LoginModel.getCX(), + cm = LoginModel.getCM(), + dataObj = { + api_key: apiKey, + commands_json: stringified, + commands_hash: md5tostring, + ver: 1, + https: 1 + }, + jsnstr = JSON.stringify(dataObj); + return $http({ + url: url, + method: "POST", + data: "api=" + jsnstr + "&apicx=" + cx + "&apicm=" + cm, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }).then(function(result) { + return ViewStateModel.allowPreviews = !0, result.data + }) + } + } +}]), app.factory("myHttpInterceptor", ["$q", "$rootScope", "ViewStateModel", function($q, $rootScope, ViewStateModel) { + return { + response: function(response) { + var errorFree = !0; + return "POST" === response.config.method && (response.data.e ? (console.log("Apiservice myHttpInterceptor error >>>", response.data), errorFree = !1) : response.data.commands && response.data.commands.forEach(function(entry) { + if (entry && entry.hasOwnProperty("e")) { + if (response.config.data && -1 != response.config.data.indexOf("userinfo")) console.log("myHttpInterceptor user info, do not show alert ", response); + else if (103 === response.data.commands[0].c) response.data.commands[0].a && (console.log("APIService myHttpInterceptor alreadyBought or onwClips", response.data.commands[0].a), 0 < response.data.commands[0].a.bought_before.length && ($rootScope.$emit("alreadyBought", response.data.commands[0].a.bought_before), console.log("APIService myHttpInterceptor alreadyBought", response.data.commands[0].a.bought_before)), 0 < response.data.commands[0].a.ownClips.length && ($rootScope.$emit("ownClips", response.data.commands[0].a.ownClips), console.log("APIService myHttpInterceptor ownClips", response.data.commands[0].a.ownClips))); + else { + console.log("myHttpInterceptor modal simple requested :", entry), "You are not logged in" == entry.s.split(": ")[1] ? $rootScope.$emit("modal not logged in", [ERROR]) : $rootScope.$emit("modal simple requested", [ERROR, entry.s.split(": ")[1]]) + } + errorFree = !1 + } + })), errorFree ? response : $q.reject(response) + }, + responseError: function(response) { + return response.config.url == MODAL_INTRO_HTML || response.config.url == MODAL_CHOOSE_BILLING_INFO_HTML ? console.log("apiService don't show error modal for ", response.config.url) : ($rootScope.$emit("modal simple requested", [ERROR, response.headers().status]), console.log("apiService don't show error modal but response ", response)), $q.reject(response) + } + } +}]), app.config(function($httpProvider) { + $httpProvider.interceptors.push("myHttpInterceptor") +}), app.service("CheckOutService", ["CartModel", "UserModel", "Service", function(CartModel, UserModel, Service) { + this.onCheckOutRequested = function(buyAnyway) { + console.log("CheckOutService total before VAT: ", CartModel.cartTotal.subtotals.afterVat), console.log("CheckOutService credits: ", CartModel.cartTotal.creditsData.availableSum), console.log("CheckOutService buyAnyway: ", buyAnyway), CartModel.cartTotal.creditsData.availableSum < CartModel.cartTotal.subtotals.afterVat ? Service.purchaseWithCash(buyAnyway) : Service.purchaseWithCredits(buyAnyway) + } +}]), app.service("CreateOnFileSystemService", ["AppModel", "CreateFileCompleteService", function(AppModel, CreateFileCompleteService) { + var call = { + createUserHomeFolder: function() { + call.createDir(AppModel.getDirHomePond5()) + }, + createUserSubFolders: function() { + console.log("CreateOnFileSystemService createUserSubFolders", AppModel.getDirDefaultLib()); + for (var dirs = [AppModel.getDirImports(), AppModel.getDirPrefs(), AppModel.getDirDefaultLib(), AppModel.getDirDestinations(), AppModel.getDirUser()], i = 0; i < dirs.length; i++) { + var dir = dirs[i]; + call.createDir(dir) + } + }, + createDestinationBaseFolder: function() { + call.createDir(AppModel.currentBaseFolder + path.sep + "pond5", !0) + }, + createDestinationFolders: function() { + AppModel.previewsDir = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + "previews", AppModel.purchasedDir = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + "purchased", call.createDir(AppModel.previewsDir), call.createDir(AppModel.purchasedDir) + }, + createDir: function(dir, isDestination) { + fs.exists(dir, function(exists) { + exists ? call.onDirReady(dir, isDestination) : fs.mkdir(dir, 511, function(err) { + if (err) throw err; + call.onDirReady(dir, isDestination) + }) + }) + }, + onDirReady: function(dir, isDestination) { + if (isDestination = isDestination || !1) this.createDestinationFolders(); + else { + var filePath, xml; + switch (dir) { + case AppModel.getDirHomePond5(): + call.createUserSubFolders(); + break; + case AppModel.getDirImports(): + filePath = "imported_previews.xml", xml = ''; + break; + case AppModel.getDirPrefs(): + filePath = "preferences.xml", xml = ''; + break; + case AppModel.getDirUser(): + filePath = "user.xml", xml = ''; + break; + case AppModel.getDirDestinations(): + filePath = "destinations.xml", xml = ''; + break; + case AppModel.getDirDefaultLib(): + filePath = "defaultLib.xml", xml = ''; + break; + case AppModel.currentBaseFolder: + this.createDestinationFolders(); + break; + default: + return + } + filePath && call.createFile(dir + path.sep + filePath, '' + xml) + } + }, + createFile: function(file, content) { + fs.exists(file, function(exists) { + exists ? CreateFileCompleteService.onFileReady(file) : fs.writeFile(file, content, function(err) { + if (err) throw err; + console.log("CreateOnFileSystemService, created file: ", file), CreateFileCompleteService.onFileReady(file) + }) + }) + } + }; + return call +}]), app.service("DeleteOnFileSystemService", [function() { + return { + deleteFiles: function(items) { + items.forEach(function(item) { + var file = item.downloadDestination + item.fileName; + fs.exists(file, function(exists) { + exists && fs.unlink(file, function(err) { + if (err) throw err + }) + }) + }) + }, + deleteFolder: function(folders, cb) { + console.log("DeleteOnFileSystemService deleteFolder, folders, length:", folders.length), folders.forEach(function(folder) { + console.log("DeleteOnFileSystemService deleteFolder, folder:", folder), fs.exists(folder, function(exists) { + exists ? rimraf(folder, function(err) { + if (err) throw err; + console.log("DeleteOnFileSystemService deleteFolder deleted: ", folder), cb() + }) : (console.log("DeleteOnFileSystemService deleteFile folder does not exist:", folder), cb()) + }) + }) + } + } +}]), app.factory("DownloadBatchService", ["Service", "PurchasesModel", "DownloadModel", function(Service, PurchasesModel, DownloadModel) { + return { + onBatchRequested: function(purchases) { + var j, i; + for (purchases = purchases || PurchasesModel.purchasesVO.items, i = 0; i < purchases.length; i += 1) + for (j = 0; j < PurchasesModel.purchasesVO.items.length; j += 1) purchases[i].id == PurchasesModel.purchasesVO.items[j].id && (purchases[i] = PurchasesModel.purchasesVO.items[j]); + for (DownloadModel.downloadingBatchURLs = !0, purchases = purchases.filter(function(v, i, a) { + return a.indexOf(v) == i + }), i = 0; i < purchases.length; i += 1) Service.getPurchaseURL(purchases[i].id, purchases[i].transactionID, purchases[i].versionID, purchases[i].version) + } + } +}]), app.service("DownloadCancelService", ["$rootScope", "DeleteOnFileSystemService", "ProgressService", "DownloadModel", function($rootScope, DeleteOnFileSystemService, ProgressService, DownloadModel) { + return { + onCancelSingle: function(item) { + console.log("DownloadCancelService onCancelSingle: ", item, item.downloadType), item.canceled = !0, $rootScope.$emit("cancel download", item), ProgressService.clearItem(item), DeleteOnFileSystemService.deleteFiles([item]), item.downloading && (item.downloading = !1, DownloadModel.downloadCounter--); + for (var len = DownloadModel.itemsDownloadList.length; len--;) + if (DownloadModel.itemsDownloadList[len].fileName === item.fileName) { + var removal = DownloadModel.itemsDownloadList[len]; + DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { + return itm !== removal + }) + } console.log("DownloadCancelService onCancelSingle num of items: ", DownloadModel.itemsDownloadList.length), $rootScope.$emit("modal simple requested", ["", "Download of " + item.fileName + " has been canceled."], "sm") + }, + onCancelAll: function() { + console.log("DownloadCancelService cancel all downloads", DownloadModel.itemsDownloadList); + for (var len = DownloadModel.itemsDownloadList.length; len--;) { + var item = DownloadModel.itemsDownloadList[len]; + 100 !== item.progressPerc && (item.canceled = !0, $rootScope.$emit("cancel download", item), ProgressService.clearItem(item), DeleteOnFileSystemService.deleteFiles([item])) + } + $rootScope.$emit("modal simple requested", ["", "All incomplete downloads have been canceled and deleted."], "sm"), DownloadModel.downloadCounter = -1, DownloadModel.itemsDownloadList = [] + } + } +}]), app.service("DownloadCompleteService", ["$rootScope", "UnzipService", function($rootScope, UnzipService) { + return { + onComplete: function(items) { + UnzipService.unzipItems(items) + } + } +}]), app.service("DownloadRequestService", ["$rootScope", "DownloadService", "ProgressService", "DownloadModel", "ReplaceModel", "AppModel", "ImportService", "ReplaceService", "StayAwakeService", "UnzipService", function($rootScope, DownloadService, ProgressService, DownloadModel, ReplaceModel, AppModel, ImportService, ReplaceService, StayAwakeService, UnzipService) { + $rootScope.$on("download requested", function(event, items) { + var downloadFolderName; + console.log("DownloadRequestService DownloadModel.itemsDownloadList: ", DownloadModel.itemsDownloadList), "preview" === items[0].downloadType ? downloadFolderName = "previews" : "purchase" === items[0].downloadType && (downloadFolderName = "purchased"); + var item, dest = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + downloadFolderName + path.sep; + console.log("DownloadRequestService downloadRequested items:", items), $rootScope.$emit("scroll progress to top"); + for (var i = 0; i < items.length; i++) { + var codec; + (item = items[i]).downloadDestination = dest, "preview" === item.downloadType ? "Video" == item.type || "AE" == item.type ? item.downloadURL = item.h264URL : "Sound effect" == item.type || "Music" == item.type ? item.downloadURL = item.m4aURL : "Photo" != item.type && "Illustration" != item.type || (item.downloadURL = item.iconLargeURL) : "purchase" === item.downloadType && (item.downloadURL = item.hiresURL), "Photo" == item.type ? item.ext = "jpg" : item.ext = item.downloadURL.substr(item.downloadURL.lastIndexOf(".") + 1).split("?")[0], item.videoCodec && (codec = item.videoCodec), "preview" !== item.downloadType && "unknown" !== codec && void 0 !== codec || (codec = ""), item.fileName = getFormattedName(item.id + " " + codec + " " + item.name + "." + item.ext), item.progressName = getAbbrName(item.fileName, 20), "preview" === item.downloadType && "AE" === item.vs && (item.fileName = "AE " + item.fileName), "purchase" === item.downloadType && ("AE" === item.vs ? item.fileName = "AE " + item.fileName : item.fileName = "hires " + item.fileName), $rootScope.$emit("open progress", !1), item.progressPerc = "", item.progressMB = "", ProgressService.addItem(item) + } + $rootScope.$$listenerCount["on item downloaded"] || $rootScope.$on("on item downloaded", function(event) { + DownloadModel.downloadCounter++, console.log("DownloadRequestService on item downloaded DownloadModel.downloadCounter: ", DownloadModel.downloadCounter), console.log("DownloadRequestService on item downloaded DownloadModel.itemsDownloadList: ", DownloadModel.itemsDownloadList); + var item = DownloadModel.itemsDownloadList[DownloadModel.downloadCounter]; + if (item) { + StayAwakeService.updateState(!0); + new DownloadService.download(item) + } else if (StayAwakeService.updateState(!1), DownloadModel.downloadCounter--, console.log("DownloadRequestService download complete, check if something needs to be done, complete previews", ProgressService.getCompletedPreviews()), ProgressService.getCompletedPreviewsStatus() && ImportService.importClips(ProgressService.getCompletedPreviews()), ProgressService.getCompletedPurchasesStatus()) { + console.log("DownloadRequestService purchases completed: ", ProgressService.getCompletedPurchases()), console.log("DownloadRequestService purchases completed ReplaceModel.getState(): ", ReplaceModel.getState()); + var AEItems = []; + if (ProgressService.getCompletedPurchases().forEach(function(item) { + "AE" == item.type && AEItems.push(item) + }), "1.0.8" != PLUGIN_VERSION && UnzipService.unzipItems(AEItems), ReplaceModel.getState() === NOT_DOWNLOADED) { + var dest = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + "purchased" + path.sep; + ProgressService.getCompletedPurchases().forEach(function(entry) { + ReplaceModel.addHires(dest, [entry]) + }), ReplaceService.onPurchasedAndDownloaded(AEItems.length) + } + } + }), console.log("DownloadRequestService new request, ProgressService.getIncompleteItems ", ProgressService.getIncompleteItems()), 0 < ProgressService.getIncompleteItems().length && !ProgressService.getDownloadingStatus() && $rootScope.$emit("on item downloaded") + }) +}]), app.service("DownloadService", ["$rootScope", "ProgressService", function($rootScope, ProgressService) { + function download(item) { + console.log("DownloadService download item: ", item); + var allowWriting = !0; + $rootScope.$on("cancel download", function(event, itm) { + itm.fileName === item.fileName && (itm.canceled = !0, item.canceled = !0, allowWriting = !1) + }), item.downloading = !0; + var file, sizeOnFS, writeOptions, path = item.downloadDestination + item.fileName; + writeOptions = fs.existsSync(path) ? (sizeOnFS = fs.statSync(path).size, console.log("DownloadService sizeOnFS: ", sizeOnFS), { + flags: "r+" + }) : (console.log("DownloadService file does not exist yet, create stream"), { + flags: "w" + }), file = fs.createWriteStream(path, writeOptions), https.get(item.downloadURL, function(res) { + var len; + res.headers["content-length"] ? (len = parseInt(res.headers["content-length"], 10), console.log("DownloadService res has content-length: ", res)) : console.log("DownloadService content-length unknown", res); + var progressPerc, cur = 0, + total = len / 1048576; + + function setToComplete() { + item.canceled || (item.progressPerc = 100, item.progressMB = total.toFixed(2) + "/" + total.toFixed(2) + "MB", item.completed = !0), item.canceled = !1, item.downloading = !1, $rootScope.$emit("on item downloaded"), $rootScope.$digest() + } + res.pipe(file), len <= sizeOnFS && (file.end(), setToComplete()), res.on("data", function(chunk) { + allowWriting ? (cur += chunk.length, progressPerc = (100 * cur / len).toFixed(2), $rootScope.$apply(function() { + item.progressPerc = progressPerc.split(".")[0], item.progressMB = (cur / 1048576).toFixed(2) + "/" + total.toFixed(2) + "MB" + })) : res.destroy() + }).on("error", function(e) { + console.log("DownloadService error: " + e.message) + }).on("end", function() { + file.end(), setToComplete() + }) + }).on("error", function(err) { + console.error("Download Error code and filename:", err.code, item.fileName), console.error("Download err:", err), item.progressPerc = 0, item.progressMB = "", setTimeout(function() { + download(item, options) + }, 1e3) + }) + } + return { + download: function(item, options) { + return new download(item, options) + } + } +}]), app.service("ImportAEService", ["$rootScope", "ReplaceModel", function($rootScope, ReplaceModel) { + var call = { + showingModal: !1, + import: function(sourceDir) { + var walk = function(dir, done) { + var files = []; + fs.readdir(dir, function(err, list) { + if (err) return done(err); + var i = 0; + ! function next() { + var file = list[i++]; + if (!file) return done(null, files); + file = dir + "/" + file, fs.stat(file, function(err, stat) { + stat && stat.isDirectory() ? walk(file, function(err, res) { + files = files.concat(res), next() + }) : (files.push(file), next()) + }) + }() + }) + }; + walk(sourceDir, function(err, files) { + if (err) throw err; + for (var i = 0; i < files.length; i += 1) console.log("ImportService file", files[i]), -1 != files[i].indexOf(".aep") && csInterface.evalScript("importAETemplate(" + JSON.stringify(files[i]) + ")", function(result) { + call.showingModal || ($rootScope.$emit("modal simple requested", ["", "Your project has been updated."]), call.showingModal = !0), console.log("ImportAEService import showingModal", call.showingModal) + }) + }) + } + }; + return call +}]), app.factory("ImportedPreviewsService", ["$rootScope", function($rootScope) { + var result = { + readXML: function() { + var dest = path.sep + "pond5" + path.sep + "imports" + path.sep + "imported_previews.xml"; + result.file = getUserHome() + dest, fs.readFile(result.file, "utf8", function(err, data) { + if (err) throw err; + result.xml = data, result.parseXML() + }) + }, + saveItem: function(id) { + var idsString = result.idsString.toString(); - 1 == idsString.indexOf(id.toString()) && (0 < idsString.length ? result.idsString += "," + id : result.idsString = id, result.writeToDisk()) + }, + deleteItem: function(id) { + -1 != result.idsString.indexOf(id) && (result.idsString = result.idsString.replace(id, "")), "," == result.idsString.substr(0, 1) && (result.idsString = result.idsString.substr(1)), "," == result.idsString.substr(result.idsString.length - 1, result.idsString.length) && (result.idsString = result.idsString.slice(0, -1)), result.writeToDisk(), $rootScope.$emit("api call", { + fn: "getImportedPreviews" + }) + }, + parseXML: function() { + var parser = new xml2js.Parser; + parser.addListener("end", function(res) { + (result.parsedXML = res) && (result.idsString = res.root.previews[0].$.ids) + }), parser.parseString(result.xml) + }, + writeToDisk: function() { + result.parsedXML.root.previews[0].$.ids = result.idsString; + var xml = (new xml2js.Builder).buildObject(result.parsedXML); + fs.writeFile(result.file, xml, function(err) { + if (err) throw err + }) + } + }; + return result +}]), app.service("MissingItemsService", ["$rootScope", "MissingItemsModel", "ReplaceModel", "Service", "CartModel", "ReplaceServiceShared", function($rootScope, MissingItemsModel, ReplaceModel, Service, CartModel, ReplaceServiceShared) { + $rootScope.$on("missing items complete", function(event, items) { + console.log("MissingItemsService on missing items: ", items), ReplaceModel.getState() === MISSING_ITEMS && result.onMissingItems(items) + }), $rootScope.$on("formats replacing complete", function(event, item, formats) { + ReplaceModel.getState() === MISSING_ITEMS && result.onMissingItemsFormats(item, formats) + }), $rootScope.$on("on purchases vo", function(event, vo) { + console.log("MissingItemsService on purchases vo, state: ", ReplaceModel.getState()), ReplaceModel.getState() != DEFAULT && result.onPurchasesVO(vo) + }); + var result = { + missingItemsCounter: 0, + onMissingItems: function(data) { + var missingItemsVO = new MissingItemsVO(data.commands[0]); + (MissingItemsModel.missingItemsVO = missingItemsVO).items.forEach(function(entry) { + Service.getFormatsReplacing(entry) + }) + }, + onMissingItemsFormats: function(item, formats) { + if (result.missingItemsCounter++, 1 < (formats = _.uniq(formats, function(p) { + return p.ti + })).length) + for (i = 0; i < formats.length; i++) item.formats[i] = new ItemVO(formats[i]), item.parentFormatID = item.id, item.formats[i].offset = formats[i].offset; + result.missingItemsCounter === MissingItemsModel.missingItemsVO.items.length && (result.missingItemsCounter = 0, Service.getPurchases()) + }, + onPurchasesVO: function(purchasesVO) { + for (var item, missingItems = MissingItemsModel.missingItemsVO.items, cartItems = CartModel.cartVO.items, purchasedItems = purchasesVO.items, i = 0; i < missingItems.length; i++) { + var cartItem, purchase; + item = missingItems[i]; + for (var j = 0; j < cartItems.length; j++) { + cartItem = cartItems[j], item.id == cartItem.id && (item.inCart = !0); + for (var formats = item.formats, k = 0; k < formats.length; k++) formats[k].id == cartItem.id && formats[k].offset == cartItem.offset && (formats[k].inCart = !0, item.inCart = !0) + } + for (j = 0; j < purchasedItems.length; j++) { + purchase = purchasedItems[j], item.id == purchase.id && (item.inDownloads = !0, item.transactionID = purchase.transactionID); + for (formats = item.formats, k = 0; k < formats.length; k++) formats[k].id == purchase.id && (formats[k].inDownloads = !0, formats[k].transactionID = purchase.transactionID, purchasedItems[j].parentFormatID && (formats[k].parentFormatID = purchase.parentFormatID)) + } + } + ReplaceModel.getState() === MISSING_ITEMS ? $rootScope.$emit("modal replace", missingItems) : ReplaceModel.getState() === NOT_PURCHASED && ReplaceServiceShared.onPurchased(missingItems) + } + }; + return result +}]), app.service("ProgressService", ["$rootScope", "DownloadModel", function($rootScope, DownloadModel) { + var result = { + alreadyHasItem: function(item) { + var itemsContainItem = !1; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.fileName === item.fileName && (itemsContainItem = !0) + }), itemsContainItem + }, + addItem: function(item) { + DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.fileName === item.fileName && (console.log("ProgressService already in list: ", item.fileName), item.completed = !1, item.imported = !1, item.canceled = !1, item.progressPerc = 0, item.progressMB = "", DownloadModel.downloadCounter--, result.clearItem(item), console.log("ProgressService already in list, cleared: ", DownloadModel.itemsDownloadList)) + }), DownloadModel.itemsDownloadList.push(item), console.log("ProgressService addItem, list: ", DownloadModel.itemsDownloadList), $rootScope.$emit("added to progress") + }, + clearCompleteItems: function() { + console.log("ProgressService clearCompleteItems "); + for (var len = DownloadModel.itemsDownloadList.length, oldLen = len; len--;) { + var item = DownloadModel.itemsDownloadList[len]; + if (100 === item.progressPerc) { + item.completed = !1, item.imported = !1, item.canceled = !1, item.progressPerc = 0; + var removal = DownloadModel.itemsDownloadList[len]; + DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { + return itm !== removal + }) + } + } + var diff = oldLen - DownloadModel.itemsDownloadList.length; + DownloadModel.downloadCounter = DownloadModel.downloadCounter - diff, console.log("ProgressService clearCompleteItems DownloadModel.itemsDownloadList: ", DownloadModel.itemsDownloadList), console.log("ProgressService clearCompleteItems new downloadCounter: ", DownloadModel.downloadCounter), $rootScope.$emit("clear progress") + }, + clearIncompleteItems: function() { + console.log("ProgressService clearIncompleteItems "); + for (var len = DownloadModel.itemsDownloadList.length; len--;) + if (100 !== DownloadModel.itemsDownloadList[len].progressPerc) { + var removal = DownloadModel.itemsDownloadList[len]; + DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { + return itm !== removal + }) + } $rootScope.$emit("on clear", DownloadModel.itemsDownloadList) + }, + clearAllItems: function() { + console.log("ProgressService clearAllItems "), DownloadModel.itemsDownloadList = [], $rootScope.$emit("clear progress"), DownloadModel.downloadCounter = 0 + }, + clearItem: function(item) { + console.log("ProgressService clearItem "); + for (var len = DownloadModel.itemsDownloadList.length; len--;) + if (DownloadModel.itemsDownloadList[len].fileName === item.fileName) { + var removal = DownloadModel.itemsDownloadList[len]; + DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { + return itm !== removal + }) + } $rootScope.$emit("clear progress") + }, + getIncompleteItems: function() { + var incompletes = []; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.completed || (console.log("ProgressService not completed: ", entry.fileName), incompletes.push(entry)) + }), incompletes + }, + getCompletedPreviewsStatus: function() { + var allCompleted = !0; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.completed || "preview" !== entry.downloadType || (allCompleted = !1) + }), 0 === DownloadModel.itemsDownloadList.length && (allCompleted = !1), console.log("ProgressService getCompletedPreviewsStatus allCompleted", allCompleted), allCompleted + }, + getCompletedPreviews: function() { + var completes = []; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.completed && "preview" == entry.downloadType && completes.push(entry) + }), completes + }, + getCompletedPurchasesStatus: function() { + var allCompleted = !0; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.completed || "purchase" !== entry.downloadType || (allCompleted = !1) + }), 0 === DownloadModel.itemsDownloadList.length && (allCompleted = !1), console.log("ProgressService getCompletedPurchasesStatus allCompleted", allCompleted), allCompleted + }, + getCompletedPurchases: function() { + var completes = []; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.completed && "purchase" == entry.downloadType && completes.push(entry) + }), completes + }, + getDownloadingStatus: function() { + var downloading = !1; + return DownloadModel.itemsDownloadList.forEach(function(entry) { + entry.downloading && (downloading = !0) + }), downloading + } + }; + return result +}]), app.service("ReadClipsOnFSService", ["$rootScope", "ReplaceModel", "MissingItemsModel", "ViewStateService", "DownloadBatchService", "AppModel", function($rootScope, ReplaceModel, MissingItemsModel, ViewStateService, DownloadBatchService, AppModel) { + var call = { + listPurchasesOnFS: function(cb) { + ReplaceModel.hiresOnFS = []; + for (var cbCounter = 0, i = 0; i < AppModel.baseFolders.length; i++) call.readPurchasesFolders(AppModel.baseFolders[i] + path.sep + "pond5" + path.sep + "purchased" + path.sep, function() { + ++cbCounter === AppModel.baseFolders.length && (console.log("\nReadClipsOnFSService ReplaceModel.hiresOnFS done: ", cbCounter, ReplaceModel.hiresOnFS), call.listPreviewsOnFS(function() { + cb() + })) + }) + }, + readPurchasesFolders: function(dest, cb) { + fs.readdir(dest, function(err, files) { + if (err) throw new Error("ReadClipsOnFSService: " + dest + " does not exist."); + var hiresVO; + files = files.filter(junk.not); + for (var i = 0; i < files.length; i += 1) hiresVO = new HiresVO(dest, files[i]), ReplaceModel.hiresOnFS.push(hiresVO), 0 === path.extname(files[i]).length ? hiresVO.type = "AE folder" : ".zip" === path.extname(files[i]) ? hiresVO.type = "AE zip" : ".mov" === path.extname(files[i]) ? hiresVO.type = "video" : ".wav" === path.extname(files[i]) && (hiresVO.type = "audio"); + cb() + }) + }, + listPreviewsOnFS: function(cb) { + ReplaceModel.previewsOnFS = []; + for (var i = 0; i < AppModel.baseFolders.length; i++) { + var walk = function(dir, done) { + var files = []; + fs.readdir(dir, function(err, list) { + if (err) return done(err); + var i = 0; + ! function next() { + var file = list[i++]; + if (!file) return done(null, files); + file = dir + "/" + file, fs.stat(file, function(err, stat) { + stat && stat.isDirectory() ? walk(file, function(err, res) { + files = files.concat(res), next() + }) : (files.push(file), next()) + }) + }() + }) + }, + dest = AppModel.baseFolders[i] + path.sep + "pond5" + path.sep + "previews", + counter = 0; + walk(dest, function(err, files) { + if (err) throw err; + for (var previewVO, i = 0; i < files.length; i += 1) previewVO = new PreviewVO(dest, files[i]), ReplaceModel.previewsOnFS.push(previewVO); + ++counter === AppModel.baseFolders.length && cb() + }) + } + } + }; + return call +}]), app.service("ReplaceServiceShared", ["$rootScope", "ReplaceModel", "Service", "MissingItemsModel", "ViewStateService", "DownloadBatchService", "ImportAEService", "DeleteOnFileSystemService", function($rootScope, ReplaceModel, Service, MissingItemsModel, ViewStateService, DownloadBatchService, ImportAEService, DeleteOnFileSystemService) { + var call = { + removeDuplicates: function(clips) { + return clips = clips.filter(function(v, i, a) { + return a.indexOf(v) === i + }) + }, + getPreviewsOnFSNames: function() { + var previewNamesonFS = []; + return ReplaceModel.previewsOnFS.forEach(function(entry) { + previewNamesonFS.push(entry.name) + }), previewNamesonFS + }, + filterNonP5Clips: function(clips, previewNamesOnFS) { + return clips = clips.filter(function(n) { + return -1 != previewNamesOnFS.indexOf(n) + }) + }, + getPreviewsIDs: function(clips) { + var previewIDs = []; + return clips.forEach(function(entry) { + var substr = entry.split(" "); + "AE" === substr[0] ? previewIDs.push(substr[1]) : previewIDs.push(substr[0]) + }), console.log("\nReplaceServiceShared previewIDs: " + previewIDs), previewIDs + }, + setReplaceProp: function(ids) { + for (var i = 0; i < ids.length; i++) + for (var j = 0; j < ReplaceModel.hiresOnFS.length; j++) ids[i] === ReplaceModel.hiresOnFS[j].id && (ReplaceModel.hiresOnFS[j].replace = !0) + }, + getMissingItemIDs: function(clipsInSeqs) { + var clipsInSelectedSequences = clipsInSeqs; + console.log("ReplaceService ReplaceModel.aeItemsinProjectView: ", ReplaceModel.getAEItems()), 0 < ReplaceModel.getAEItems().length && (clipsInSelectedSequences = clipsInSelectedSequences.concat(ReplaceModel.getAEItems())), console.log("ReplaceService clips after concat layer items and AE items: ", clipsInSelectedSequences), clipsInSelectedSequences = call.removeDuplicates(clipsInSelectedSequences), console.log("\nReplaceServiceShared clipsInSelectedSequences after removing duplicates: ", clipsInSelectedSequences); + var previewNamesonFS = call.getPreviewsOnFSNames(); + console.log("\nReplaceServiceShared previewNamesonFS: ", previewNamesonFS), clipsInSelectedSequences = call.filterNonP5Clips(clipsInSelectedSequences, previewNamesonFS), console.log("\nReplaceServiceShared after filterNonP5Clips", clipsInSelectedSequences); + var previewIDs = call.getPreviewsIDs(clipsInSelectedSequences); + console.log("\nReplaceServiceShared previewIDs: " + previewIDs), call.setReplaceProp(previewIDs), console.log("\nReplaceServiceShared after set replace: " + ReplaceModel.hiresOnFS); + var hiresIDs = call.getHiresIDsonFS(); + console.log("\nReplaceServiceShared hiresIDs: " + hiresIDs); + var missingItemIDs = _(previewIDs).difference(hiresIDs), + missingIDsToString = missingItemIDs.join(","); + if (console.log("nReplaceServiceShared missingIDsToString: " + missingIDsToString), 0 < missingItemIDs.length) Service.getMissingItems(missingIDsToString); + else { + if (0 < hiresIDs.length) return hiresIDs.length; + 0 === clipsInSelectedSequences.length && (ReplaceModel.setState(DEFAULT), $rootScope.$emit("modal simple requested", ["", "There are no Pond5 previews in your current project."])) + } + }, + getHiresIDsonFS: function() { + var hiresIDs = []; + return ReplaceModel.hiresOnFS.forEach(function(entry) { + (entry.replace || entry.importAE) && hiresIDs.push(entry.id) + }), hiresIDs + }, + onModalReplaceOK: function() { + for (var item, missingItems = MissingItemsModel.missingItemsVO.items, itemsNotPurchased = [], itemsNotDownloaded = [], i = 0; i < missingItems.length; i++)(item = missingItems[i]).selected && !item.inDownloads && itemsNotPurchased.push(item), item.selected && item.inDownloads && itemsNotDownloaded.push(item); + 0 < itemsNotPurchased.length ? call.onNotPurchased(itemsNotPurchased) : 0 < itemsNotDownloaded.length ? (console.log("ReplaceServiceShared onModalReplaceOK, download items: ", itemsNotDownloaded), ReplaceModel.missingDownloads = itemsNotDownloaded, call.onNotDownloaded(itemsNotDownloaded)) : (ReplaceModel.setState(PURCHASED_AND_DOWNLOADED), console.log("ReplaceServiceShared onModalReplaceOK, replace"), call.onPurchasedAndDownloaded()) + }, + onNotPurchased: function(itemsNotPurchased) { + for (var addToCartItems = [], i = 0; i < itemsNotPurchased.length; i++) + if (item = itemsNotPurchased[i], 0 < itemsNotPurchased[i].formats.length) + for (var j = 0; j < itemsNotPurchased[i].formats.length; j++) format = itemsNotPurchased[i].formats[j], format.selected && (console.log("ReplaceServiceShared onNotPurchased add this format to cart: ", format), addToCartItems.push(format.id)); + else console.log("ReplaceServiceShared onNotPurchased add this item to cart: ", item), addToCartItems.push(item.id); + $rootScope.$emit("modal simple requested", ["", "Please review your Cart. Press the 'Checkout' button to proceed with replacing your previews."]); + var apiObj = { + fn: "modifyCart", + args: [addToCartItems.join(","), ""] + }; + $rootScope.$emit("api call", apiObj), ViewStateService.viewRequested("cart"), ReplaceModel.setState(NOT_PURCHASED) + }, + onPurchased: function(downloadItems) { + console.log("ReplaceServiceShared onPurchased: ", downloadItems); + for (var item, missingItems = MissingItemsModel.missingItemsVO.items, itemsNotDownloaded = [], i = 0; i < missingItems.length; i++)(item = missingItems[i]).inDownloads && itemsNotDownloaded.push(item); + 0 < itemsNotDownloaded.length && (console.log("ReplaceServiceShared onPurchased, download items: ", itemsNotDownloaded), ReplaceModel.missingDownloads = itemsNotDownloaded, $rootScope.$emit("modal simple requested", ["Your purchase has been successful.", "Your purchased clips will begin downloading now. Once the downloads are completed, your lo-res previews will be replaced with your high-res clips."]), call.onNotDownloaded(itemsNotDownloaded, !0)) + }, + onNotDownloaded: function(itemsNotDownloaded, afterPurchase) { + afterPurchase = afterPurchase || !1, console.log("ReplaceServiceShared onNotDownloaded missing items:", itemsNotDownloaded); + for (var downloadItems = [], i = 0; i < itemsNotDownloaded.length; i++) + if (item = itemsNotDownloaded[i], 0 < itemsNotDownloaded[i].formats.length) + for (var j = 0; j < itemsNotDownloaded[i].formats.length; j++) format = itemsNotDownloaded[i].formats[j], format.selected && (console.log("ReplaceServiceShared onNotDownloaded download this format: ", format), downloadItems.push(format)); + else console.log("ReplaceServiceShared onNotDownloaded download item: ", item), downloadItems.push(item); + afterPurchase || $rootScope.$emit("modal simple requested", ["You have purchases that are missing in your project. ", "They will be downloaded. Once the downloads are completed, your lo-res previews will be replaced with your high-res clips."]), DownloadBatchService.onBatchRequested(downloadItems), ReplaceModel.setState(NOT_DOWNLOADED) + } + }; + return call +}]), app.service("ScrollService", ["SearchModel", "Service", function(SearchModel, Service) { + this.onScroll = function() { + if (SearchModel.allowInfiniteScroll) { + var m = document.getElementById("main-holder"); + 1 === (getScroll()[1] - 72) / (m.scrollHeight - window.innerHeight) && (console.log("ScrollService show more: " + SearchModel.isSearching), SearchModel.isSearching || (SearchModel.isSearching = !0, SearchModel.resultType = "add", SearchModel.page = SearchModel.page + 1, Service.search())) + } + } +}]), app.factory("StartUpService", ["$rootScope", "CreateOnFileSystemService", "MissingItemsService", "ViewStateService", "AppModel", function($rootScope, CreateOnFileSystemService, MissingItemsService, ViewStateService, AppModel) { + return $("#logo").click(function() { + location.reload() + }), $rootScope.$on("environment set", function() { + console.log("StartUpService, 26/10 pointing at ", window.location.href), gup("tp", window.location.href) && (THIRD_PARTY = gup("tp", window.location.href)), -1 < window.location.href.indexOf("test") ? MODE = "test" : MODE = "live", console.log("StartUpService MODE:", MODE), console.log("StartUpService OS:", os.platform()), console.log("StartUpService, app version: ", PLUGIN_VERSION), AppModel.currentBaseFolder = AppModel.getDocumentsPath(), console.log("StartUpService currentBaseFolder: ", AppModel.currentBaseFolder + "\n\n"), CreateOnFileSystemService.createUserHomeFolder(), MissingItemsService.missingItemsCounter = 0, ViewStateService.viewRequested("search") + }), { + init: function() { + setTimeout(function() { + AppModel.setEnv() + }, 2e3) + } + } +}]), app.factory("StayAwakeService", ["$rootScope", "DownloadModel", function($rootScope, DownloadModel) { + return { + updateState: function(state) { + console.log("StayAwakeService state: ", state), state && !DownloadModel.stayAwake ? (sleep.prevent(), DownloadModel.stayAwake = !0) : !state && DownloadModel.stayAwake && (sleep.allow(), DownloadModel.stayAwake = !1) + } + } +}]), app.service("TransactionService", ["$q", "ViewStateService", "Service", "ReplaceModel", "AnalyticsService", "CartModel", function($q, ViewStateService, Service, ReplaceModel, AnalyticsService, CartModel) { + this.onMessageReceivedFromAdyen = function(event) { + console.log("event.source: ", event.source), console.log("event origin: ", event.origin), console.log("event data: ", event.data); + var deferred = $q.defer(); + switch (event.data) { + case "PAID": + console.log("TransactionService PAID"), deferred.resolve("PAID"), ReplaceModel.getState() === NOT_PURCHASED ? Service.getPurchases() : ViewStateService.viewRequested("downloads"), AnalyticsService.sendData(null, "transaction"), Service.getUserInfo(); + break; + case "CANCELED": + deferred.reject("CANCELED"), console.log("TransactionService CANCELED"); + break; + case "PENDING": + console.log("TransactionService PENDING"), deferred.reject("PENDING"); + break; + default: + deferred.reject("UNKNOWN") + } + return deferred.promise + } +}]), app.service("UnzipService", ["$rootScope", "DeleteOnFileSystemService", "ReplaceModel", "ImportAEService", function($rootScope, DeleteOnFileSystemService, ReplaceModel, ImportAEService) { + var call = { + unzippedCounter: 0, + deletedCounter: 0, + numOfItems: 0, + items: [], + deleteObjects: [], + itemObjects: [], + unzipItems: function(items) { + call.unzippedCounter = 0, call.deletedCounter = 0, call.numOfItems = items.length, call.items = items, call.deleteObjects = [], call.itemObjects = [], call.items.forEach(function(item) { + var itemObj = { + dest: item.downloadDestination + "AE " + item.id, + source: item.downloadDestination + item.fileName + }; + call.itemObjects.push(itemObj), call.deleteObjects.push(itemObj.source, itemObj.dest + path.sep + "__MACOSX"), call.unzip(itemObj) + }), console.log("UnzipService unzipItems numOfItems:", call.numOfItems), console.log("UnzipService unzipItems call.deleteObjects:", call.deleteObjects), console.log("UnzipService unzipItems call.deleteObjects.length:", call.deleteObjects.length) + }, + unzip: function(itemObj) { + var unzipper = new DecompressZip(itemObj.source); + unzipper.on("error", function(err) { + console.log("UnzipService Caught an error: ", err) + }), unzipper.on("extract", function(log) { + console.log("UnzipService Finished extracting"), call.unzippedCounter++, call.unzippedCounter === call.numOfItems && (console.log("UnzipService Finished extracting all items, unzippedCounter", call.unzippedCounter), DeleteOnFileSystemService.deleteFolder(call.deleteObjects, function() { + console.log("UnzipService zip or mac os folder deleted"), call.deletedCounter++, console.log("UnzipService call.deletedCounter: ", call.deletedCounter), console.log("UnzipService call.deleteObjects.length: ", call.deleteObjects.length), call.deletedCounter === call.deleteObjects.length && (console.log("UnzipService ALL zip or mac os folders deleted", ReplaceModel.getState()), call.itemObjects.forEach(function(item) { + ReplaceModel.getState() === NOT_DOWNLOADED && "AEFT" == HOST_NAME && ImportAEService.import(item.dest) + }), ReplaceModel.getState() === DEFAULT && 1 < call.numOfItems ? opn(call.items[0].downloadDestination) : ReplaceModel.getState() === DEFAULT && 1 === call.numOfItems && (console.log("UnzipService opn finder"), opn(itemObj.dest)), ReplaceModel.setState(DEFAULT)) + })) + }), unzipper.on("progress", function(fileIndex, fileCount) { + console.log("UnzipService Extracted file " + (fileIndex + 1) + " of " + fileCount) + }), unzipper.extract({ + path: itemObj.dest + }) + } + }; + return call +}]), app.factory("UserService", ["$rootScope", "AppModel", "LoginModel", function($rootScope, AppModel, LoginModel) { + var file, parsedLocalXML, cm, cx, result = { + readXML: function() { + file = AppModel.getUserXML(), fs.readFile(file, "utf8", function(err, data) { + if (err) throw err; + result.parseLocalXML(data) + }) + }, + saveData: function(cx, cm) { + parsedLocalXML.root.user[0].$.cm = cm, parsedLocalXML.root.user[0].$.cx = cx, result.writeToDisk() + }, + parseLocalXML: function(xml) { + var parser = new xml2js.Parser; + parser.addListener("end", function(res) { + if (cm = (parsedLocalXML = res).root.user[0].$.cm, cx = res.root.user[0].$.cx, 0 < cm.length && 0 < cx.length) { + LoginModel.setCX(cx), LoginModel.setCM(cm); + $rootScope.$emit("api call", { + fn: "getUserInfo" + }) + } + }), parser.parseString(xml) + }, + writeToDisk: function() { + var xml = (new xml2js.Builder).buildObject(parsedLocalXML); + fs.writeFile(file, xml, function(err) { + if (err) throw err + }) + } + }; + return result +}]), app.factory("ViewStateService", ["$rootScope", "ViewStateModel", "ReplaceModel", "LoginModel", function($rootScope, ViewStateModel, ReplaceModel, LoginModel) { + var requestedState, result = { + viewRequested: function(state) { + console.log("ViewStateService viewRequested: ", state), "downloads" !== (requestedState = state) && "previews" !== requestedState && "cart" !== requestedState || LoginModel.getLoggedIn() ? (ViewStateModel.setState(state), result.onViewApproved(!0)) : $rootScope.$emit("modal not logged in", [ERROR]) + }, + onViewApproved: function(result) { + if (console.log("ViewStateService onViewApproved ", result, requestedState), result) { + var fName; + switch (ViewStateModel.setState(requestedState), requestedState) { + case "downloads": + fName = "getPurchases"; + break; + case "previews": + fName = "getImportedPreviews"; + break; + case "cart": + fName = "getCart"; + break; + case "freebies": + fName = "getFreeClips"; + break; + case "bins": + fName = "getBin"; + break; + case "search": + default: + fName = "search" + } + $rootScope.$emit("api call", { + fn: fName + }) + } else console.log("ViewStateService onViewApproved cancel clicked in modal, stay in current view") + } + }; + return result +}]); +var imgHeight, imgWidth, COUNTRIES = [{ + name: "United States", + code: "US" + }, { + name: "Afghanistan", + code: "AF" + }, { + name: "Aland Islands", + code: "AX" + }, { + name: "Albania", + code: "AL" + }, { + name: "Algeria", + code: "DZ" + }, { + name: "American Samoa", + code: "AS" + }, { + name: "Andorra", + code: "AD" + }, { + name: "Angola", + code: "AO" + }, { + name: "Anguilla", + code: "AI" + }, { + name: "Antarctica", + code: "AQ" + }, { + name: "Antigua and Barbuda", + code: "AG" + }, { + name: "Argentina", + code: "AR" + }, { + name: "Armenia", + code: "AM" + }, { + name: "Aruba", + code: "AW" + }, { + name: "Australia", + code: "AU" + }, { + name: "Austria", + code: "AT" + }, { + name: "Azerbaijan", + code: "AZ" + }, { + name: "Bahamas", + code: "BS" + }, { + name: "Bahrain", + code: "BH" + }, { + name: "Bangladesh", + code: "BD" + }, { + name: "Barbados", + code: "BB" + }, { + name: "Belarus", + code: "BY" + }, { + name: "Belgium", + code: "BE" + }, { + name: "Belize", + code: "BZ" + }, { + name: "Benin", + code: "BJ" + }, { + name: "Bermuda", + code: "BM" + }, { + name: "Bhutan", + code: "BT" + }, { + name: "Bolivia", + code: "BO" + }, { + name: "Bosnia and Herzegovina", + code: "BA" + }, { + name: "Botswana", + code: "BW" + }, { + name: "Bouvet Island", + code: "BV" + }, { + name: "Brazil", + code: "BR" + }, { + name: "British Indian Ocean Territory", + code: "IO" + }, { + name: "Brunei Darussalam", + code: "BN" + }, { + name: "Bulgaria", + code: "BG" + }, { + name: "Burkina Faso", + code: "BF" + }, { + name: "Burundi", + code: "BI" + }, { + name: "Cambodia", + code: "KH" + }, { + name: "Cameroon", + code: "CM" + }, { + name: "Canada", + code: "CA" + }, { + name: "Cape Verde", + code: "CV" + }, { + name: "Cayman Islands", + code: "KY" + }, { + name: "Central African Republic", + code: "CF" + }, { + name: "Chad", + code: "TD" + }, { + name: "Chile", + code: "CL" + }, { + name: "China", + code: "CN" + }, { + name: "Christmas Island", + code: "CX" + }, { + name: "Cocos (Keeling) Islands", + code: "CC" + }, { + name: "Colombia", + code: "CO" + }, { + name: "Comoros", + code: "KM" + }, { + name: "Congo", + code: "CG" + }, { + name: "Congo, The Democratic Republic of the", + code: "CD" + }, { + name: "Cook Islands", + code: "CK" + }, { + name: "Costa Rica", + code: "CR" + }, { + name: "Cote D'Ivoire", + code: "CI" + }, { + name: "Croatia", + code: "HR" + }, { + name: "Cuba", + code: "CU" + }, { + name: "Cyprus", + code: "CY" + }, { + name: "Czech Republic", + code: "CZ" + }, { + name: "Denmark", + code: "DK" + }, { + name: "Djibouti", + code: "DJ" + }, { + name: "Dominica", + code: "DM" + }, { + name: "Dominican Republic", + code: "DO" + }, { + name: "Ecuador", + code: "EC" + }, { + name: "Egypt", + code: "EG" + }, { + name: "El Salvador", + code: "SV" + }, { + name: "Equatorial Guinea", + code: "GQ" + }, { + name: "Eritrea", + code: "ER" + }, { + name: "Estonia", + code: "EE" + }, { + name: "Ethiopia", + code: "ET" + }, { + name: "Falkland Islands (Malvinas)", + code: "FK" + }, { + name: "Faroe Islands", + code: "FO" + }, { + name: "Fiji", + code: "FJ" + }, { + name: "Finland", + code: "FI" + }, { + name: "France", + code: "FR" + }, { + name: "French Guiana", + code: "GF" + }, { + name: "French Polynesia", + code: "PF" + }, { + name: "French Southern Territories", + code: "TF" + }, { + name: "Gabon", + code: "GA" + }, { + name: "Gambia", + code: "GM" + }, { + name: "Georgia", + code: "GE" + }, { + name: "Germany", + code: "DE" + }, { + name: "Ghana", + code: "GH" + }, { + name: "Gibraltar", + code: "GI" + }, { + name: "Greece", + code: "GR" + }, { + name: "Greenland", + code: "GL" + }, { + name: "Grenada", + code: "GD" + }, { + name: "Guadeloupe", + code: "GP" + }, { + name: "Guam", + code: "GU" + }, { + name: "Guatemala", + code: "GT" + }, { + name: "Guernsey", + code: "GG" + }, { + name: "Guinea", + code: "GN" + }, { + name: "Guinea-Bissau", + code: "GW" + }, { + name: "Guyana", + code: "GY" + }, { + name: "Haiti", + code: "HT" + }, { + name: "Heard Island and Mcdonald Islands", + code: "HM" + }, { + name: "Holy See (Vatican City State)", + code: "VA" + }, { + name: "Honduras", + code: "HN" + }, { + name: "Hong Kong", + code: "HK" + }, { + name: "Hungary", + code: "HU" + }, { + name: "Iceland", + code: "IS" + }, { + name: "India", + code: "IN" + }, { + name: "Indonesia", + code: "ID" + }, { + name: "Iran, Islamic Republic Of", + code: "IR" + }, { + name: "Iraq", + code: "IQ" + }, { + name: "Ireland", + code: "IE" + }, { + name: "Isle of Man", + code: "IM" + }, { + name: "Israel", + code: "IL" + }, { + name: "Italy", + code: "IT" + }, { + name: "Jamaica", + code: "JM" + }, { + name: "Japan", + code: "JP" + }, { + name: "Jersey", + code: "JE" + }, { + name: "Jordan", + code: "JO" + }, { + name: "Kazakhstan", + code: "KZ" + }, { + name: "Kenya", + code: "KE" + }, { + name: "Kiribati", + code: "KI" + }, { + name: "Korea, Democratic People's Republic of", + code: "KP" + }, { + name: "Korea, Republic of", + code: "KR" + }, { + name: "Kuwait", + code: "KW" + }, { + name: "Kyrgyzstan", + code: "KG" + }, { + name: "Lao People's Democratic Republic", + code: "LA" + }, { + name: "Latvia", + code: "LV" + }, { + name: "Lebanon", + code: "LB" + }, { + name: "Lesotho", + code: "LS" + }, { + name: "Liberia", + code: "LR" + }, { + name: "Libyan Arab Jamahiriya", + code: "LY" + }, { + name: "Liechtenstein", + code: "LI" + }, { + name: "Lithuania", + code: "LT" + }, { + name: "Luxembourg", + code: "LU" + }, { + name: "Macao", + code: "MO" + }, { + name: "Macedonia, The Former Yugoslav Republic of", + code: "MK" + }, { + name: "Madagascar", + code: "MG" + }, { + name: "Malawi", + code: "MW" + }, { + name: "Malaysia", + code: "MY" + }, { + name: "Maldives", + code: "MV" + }, { + name: "Mali", + code: "ML" + }, { + name: "Malta", + code: "MT" + }, { + name: "Marshall Islands", + code: "MH" + }, { + name: "Martinique", + code: "MQ" + }, { + name: "Mauritania", + code: "MR" + }, { + name: "Mauritius", + code: "MU" + }, { + name: "Mayotte", + code: "YT" + }, { + name: "Mexico", + code: "MX" + }, { + name: "Micronesia, Federated States of", + code: "FM" + }, { + name: "Moldova, Republic of", + code: "MD" + }, { + name: "Monaco", + code: "MC" + }, { + name: "Mongolia", + code: "MN" + }, { + name: "Montserrat", + code: "MS" + }, { + name: "Morocco", + code: "MA" + }, { + name: "Mozambique", + code: "MZ" + }, { + name: "Myanmar", + code: "MM" + }, { + name: "Namibia", + code: "NA" + }, { + name: "Nauru", + code: "NR" + }, { + name: "Nepal", + code: "NP" + }, { + name: "Netherlands", + code: "NL" + }, { + name: "Netherlands Antilles", + code: "AN" + }, { + name: "New Caledonia", + code: "NC" + }, { + name: "New Zealand", + code: "NZ" + }, { + name: "Nicaragua", + code: "NI" + }, { + name: "Niger", + code: "NE" + }, { + name: "Nigeria", + code: "NG" + }, { + name: "Niue", + code: "NU" + }, { + name: "Norfolk Island", + code: "NF" + }, { + name: "Northern Mariana Islands", + code: "MP" + }, { + name: "Norway", + code: "NO" + }, { + name: "Oman", + code: "OM" + }, { + name: "Pakistan", + code: "PK" + }, { + name: "Palau", + code: "PW" + }, { + name: "Palestinian Territory, Occupied", + code: "PS" + }, { + name: "Panama", + code: "PA" + }, { + name: "Papua New Guinea", + code: "PG" + }, { + name: "Paraguay", + code: "PY" + }, { + name: "Peru", + code: "PE" + }, { + name: "Philippines", + code: "PH" + }, { + name: "Pitcairn", + code: "PN" + }, { + name: "Poland", + code: "PL" + }, { + name: "Portugal", + code: "PT" + }, { + name: "Puerto Rico", + code: "PR" + }, { + name: "Qatar", + code: "QA" + }, { + name: "Reunion", + code: "RE" + }, { + name: "Romania", + code: "RO" + }, { + name: "Russian Federation", + code: "RU" + }, { + name: "Rwanda", + code: "RW" + }, { + name: "Saint Helena", + code: "SH" + }, { + name: "Saint Kitts and Nevis", + code: "KN" + }, { + name: "Saint Lucia", + code: "LC" + }, { + name: "Saint Pierre and Miquelon", + code: "PM" + }, { + name: "Saint Vincent and the Grenadines", + code: "VC" + }, { + name: "Samoa", + code: "WS" + }, { + name: "San Marino", + code: "SM" + }, { + name: "Sao Tome and Principe", + code: "ST" + }, { + name: "Saudi Arabia", + code: "SA" + }, { + name: "Senegal", + code: "SN" + }, { + name: "Serbia and Montenegro", + code: "CS" + }, { + name: "Seychelles", + code: "SC" + }, { + name: "Sierra Leone", + code: "SL" + }, { + name: "Singapore", + code: "SG" + }, { + name: "Slovakia", + code: "SK" + }, { + name: "Slovenia", + code: "SI" + }, { + name: "Solomon Islands", + code: "SB" + }, { + name: "Somalia", + code: "SO" + }, { + name: "South Africa", + code: "ZA" + }, { + name: "South Georgia and the South Sandwich Islands", + code: "GS" + }, { + name: "Spain", + code: "ES" + }, { + name: "Sri Lanka", + code: "LK" + }, { + name: "Sudan", + code: "SD" + }, { + name: "Suriname", + code: "SR" + }, { + name: "Svalbard and Jan Mayen", + code: "SJ" + }, { + name: "Swaziland", + code: "SZ" + }, { + name: "Sweden", + code: "SE" + }, { + name: "Switzerland", + code: "CH" + }, { + name: "Syrian Arab Republic", + code: "SY" + }, { + name: "Taiwan, Province of China", + code: "TW" + }, { + name: "Tajikistan", + code: "TJ" + }, { + name: "Tanzania, United Republic of", + code: "TZ" + }, { + name: "Thailand", + code: "TH" + }, { + name: "Timor-Leste", + code: "TL" + }, { + name: "Togo", + code: "TG" + }, { + name: "Tokelau", + code: "TK" + }, { + name: "Tonga", + code: "TO" + }, { + name: "Trinidad and Tobago", + code: "TT" + }, { + name: "Tunisia", + code: "TN" + }, { + name: "Turkey", + code: "TR" + }, { + name: "Turkmenistan", + code: "TM" + }, { + name: "Turks and Caicos Islands", + code: "TC" + }, { + name: "Tuvalu", + code: "TV" + }, { + name: "Uganda", + code: "UG" + }, { + name: "Ukraine", + code: "UA" + }, { + name: "United Arab Emirates", + code: "AE" + }, { + name: "United Kingdom", + code: "GB" + }, { + name: "United States", + code: "US" + }, { + name: "United States Minor Outlying Islands", + code: "UM" + }, { + name: "Uruguay", + code: "UY" + }, { + name: "Uzbekistan", + code: "UZ" + }, { + name: "Vanuatu", + code: "VU" + }, { + name: "Venezuela", + code: "VE" + }, { + name: "Vietnam", + code: "VN" + }, { + name: "Virgin Islands, British", + code: "VG" + }, { + name: "Virgin Islands, U.S.", + code: "VI" + }, { + name: "Wallis and Futuna", + code: "WF" + }, { + name: "Western Sahara", + code: "EH" + }, { + name: "Yemen", + code: "YE" + }, { + name: "Zambia", + code: "ZM" + }, { + name: "Zimbabwe", + code: "ZW" + }], + STATES = [{ + name: "Alabama", + label: "Alabama", + code: "AL" + }, { + name: "Alaska", + label: "Alaska", + code: "AK" + }, { + name: "American Samoa", + label: "American Samoa", + code: "AS" + }, { + name: "Arizona", + label: "Arizona", + code: "AZ" + }, { + name: "Arkansas", + label: "Arkansas", + code: "AR" + }, { + name: "Armed Forces Europe", + label: "Armed Forces Europe", + code: "AE" + }, { + name: "Armed Forces Pacific", + label: "Armed Forces Pacific", + code: "AP" + }, { + name: "Armed Forces the Americas", + label: "Armed Forces the Americas", + code: "AA" + }, { + name: "California", + label: "California", + code: "CA" + }, { + name: "Colorado", + label: "Colorado", + code: "CO" + }, { + name: "Connecticut", + label: "Connecticut", + code: "CT" + }, { + name: "Delaware", + label: "Delaware", + code: "DE" + }, { + name: "District of Columbia", + label: "District of Columbia", + code: "DC" + }, { + name: "Federated States of Micronesia", + label: "Federated States of Micronesia", + code: "FM" + }, { + name: "Florida", + label: "Florida", + code: "FL" + }, { + name: "Georgia", + label: "Georgia", + code: "GA" + }, { + name: "Guam", + label: "Guam", + code: "GU" + }, { + name: "Hawaii", + label: "Hawaii", + code: "HI" + }, { + name: "Idaho", + label: "Idaho", + code: "ID" + }, { + name: "Illinois", + label: "Illinois", + code: "IL" + }, { + name: "Indiana", + label: "Indiana", + code: "IN" + }, { + name: "Iowa", + label: "Iowa", + code: "IA" + }, { + name: "Kansas", + label: "Kansas", + code: "KS" + }, { + name: "Kentucky", + label: "Kentucky", + code: "KY" + }, { + name: "Louisiana", + label: "Louisiana", + code: "LA" + }, { + name: "Maine", + label: "Maine", + code: "ME" + }, { + name: "Marshall Islands", + label: "Marshall Islands", + code: "MH" + }, { + name: "Maryland", + label: "Maryland", + code: "MD" + }, { + name: "Massachusetts", + label: "Massachusetts", + code: "MA" + }, { + name: "Michigan", + label: "Michigan", + code: "MI" + }, { + name: "Minnesota", + label: "Minnesota", + code: "MN" + }, { + name: "Mississippi", + label: "Mississippi", + code: "MS" + }, { + name: "Missouri", + label: "Missouri", + code: "MO" + }, { + name: "Montana", + label: "Montana", + code: "MT" + }, { + name: "Nebraska", + label: "Nebraska", + code: "NE" + }, { + name: "Nevada", + label: "Nevada", + code: "NV" + }, { + name: "New Hampshire", + label: "New Hampshire", + code: "NH" + }, { + name: "New Jersey", + label: "New Jersey", + code: "NJ" + }, { + name: "New Mexico", + label: "New Mexico", + code: "NM" + }, { + name: "New York", + label: "New York", + code: "NY" + }, { + name: "North Carolina", + label: "North Carolina", + code: "NC" + }, { + name: "North Dakota", + label: "North Dakota", + code: "ND" + }, { + name: "Northern Mariana Islands", + label: "Northern Mariana Islands", + code: "MP" + }, { + name: "Ohio", + label: "Ohio", + code: "OH" + }, { + name: "Oklahoma", + label: "Oklahoma", + code: "OK" + }, { + name: "Oregon", + label: "Oregon", + code: "OR" + }, { + name: "Pennsylvania", + label: "Pennsylvania", + code: "PA" + }, { + name: "Puerto Rico", + label: "Puerto Rico", + code: "PR" + }, { + name: "Rhode Island", + label: "Rhode Island", + code: "RI" + }, { + name: "South Carolina", + label: "South Carolina", + code: "SC" + }, { + name: "South Dakota", + label: "South Dakota", + code: "SD" + }, { + name: "Tennessee", + label: "Tennessee", + code: "TN" + }, { + name: "Texas", + label: "Texas", + code: "TX" + }, { + name: "Utah", + label: "Utah", + code: "UT" + }, { + name: "Vermont", + label: "Vermont", + code: "VT" + }, { + name: "Virgin Islands, U.S.", + label: "Virgin Islands, U.S.", + code: "VI" + }, { + name: "Virginia", + label: "Virginia", + code: "VA" + }, { + name: "Washington", + label: "Washington", + code: "WA" + }, { + name: "West Virginia", + label: "West Virginia", + code: "WV" + }, { + name: "Wisconsin", + label: "Wisconsin", + code: "WI" + }, { + name: "Wyoming", + label: "Wyoming", + code: "WY" + }]; + +function get_browser() { + var tem, ua = navigator.userAgent, + M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; + return /trident/i.test(M[1]) ? "IE " + ((tem = /\brv[ :]+(\d+)/g.exec(ua) || [])[1] || "") : "Chrome" === M[1] && null != (tem = ua.match(/\bOPR\/(\d+)/)) ? "Opera " + tem[1] : (M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"], null != (tem = ua.match(/version\/(\d+)/i)) && M.splice(1, 1, tem[1]), M[0]) +} + +function get_browser_version() { + var tem, ua = navigator.userAgent, + M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; + return /trident/i.test(M[1]) ? "IE " + ((tem = /\brv[ :]+(\d+)/g.exec(ua) || [])[1] || "") : "Chrome" === M[1] && null != (tem = ua.match(/\bOPR\/(\d+)/)) ? "Opera " + tem[1] : (M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"], null != (tem = ua.match(/version\/(\d+)/i)) && M.splice(1, 1, tem[1]), M[1]) +} + +function findHHandWW() { + return imgHeight = this.height, imgWidth = this.width, !0 +} + +function showImage(imgPath) { + var myImage = new Image; + myImage.name = imgPath, myImage.onload = findHHandWW, myImage.src = imgPath +} + +function log(className, prefix, obj) { + if (prefix = " " + prefix + ": ", obj instanceof Array) obj.forEach(function(entry) { + log(className, "item", entry) + }); + else + for (key in console.log(className + ":"), obj) console.log(prefix + key + ": " + obj[key]), "formats" === key && obj[key].forEach(function(entry) { + log(className, " format", entry) + }), "versions" === key && obj[key].forEach(function(entry) { + log(className, " versions", entry) + }) +} + +function ExtendedID() {} + +function getAbbrName(name, len) { + return name && name.length > len ? name.slice(0, len) + "..." : name +} + +function convertArrayToCommaSeperatedString(ids) { + var idsToString = ""; + return ids.forEach(function(id) { + idsToString += id + "," + }), idsToString = idsToString.slice(0, -1) +} + +function getFormattedName(input) { + for (; - 1 != input.indexOf(",");) input = input.replace(",", " "); + for (; - 1 != input.indexOf("&");) input = input.replace("&", "and"); + for (; - 1 != input.indexOf("/");) input = input.replace("/", " "); + for (; - 1 != input.indexOf("'");) input = input.replace("'", " "); + for (; - 1 != input.indexOf("(");) input = input.replace("(", " "); + for (; - 1 != input.indexOf(")");) input = input.replace(")", " "); + for (; - 1 != input.indexOf(":");) input = input.replace(":", " "); + for (; - 1 != input.indexOf(" ");) input = input.replace(" ", " "); + return input +} + +function getUID() { + var d = (new Date).getTime(); + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + var r = (d + 16 * Math.random()) % 16 | 0; + return d = Math.floor(d / 16), ("x" == c ? r : 3 & r | 8).toString(16) + }) +} + +function getStringPosition(string, subString, index) { + return string.split(subString, index).join(subString).length +} + +function gup(name, url) { + url || (url = location.href), name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var results = new RegExp("[\\?&]" + name + "=([^&#]*)").exec(url); + return null == results ? null : results[1] +} + +function checkVersion(tv, uv) { + var updaterVersion = uv; + if (tv === updaterVersion) return !1; + var splitThis = tv.split("."), + splitThisInt = []; + splitThis.forEach(function(string) { + splitThisInt.push(parseInt(string)) + }); + var splitUpdater = updaterVersion.split("."), + splitUpdaterInt = []; + return splitUpdater.forEach(function(string) { + splitUpdaterInt.push(parseInt(string)) + }), splitUpdaterInt[0] > splitThisInt[0] || (splitUpdaterInt[0] >= splitThisInt[0] && splitUpdaterInt[1] > splitThisInt[1] || splitUpdaterInt[0] >= splitThisInt[0] && splitUpdaterInt[1] >= splitThisInt[1] && splitUpdaterInt[2] > splitThisInt[2]) +} + +function getConvertedVideoStandard(vs) { + var standard; + switch (parseInt(vs)) { + case 0: + standard = "Multimedia / Unknown"; + break; + case 1: + standard = "NTSC D1"; + break; + case 2: + standard = "NTSC DV"; + break; + case 3: + standard = "PAL / PAL DV"; + break; + case 4: + standard = "HD 1080"; + break; + case 5: + standard = "HDV 720p"; + break; + case 6: + standard = "Other Hi-Def"; + break; + case 7: + standard = "Multimedia"; + break; + case 8: + standard = "HDV 1080i"; + break; + case 9: + standard = "HD 720"; + break; + case 10: + standard = "4k+"; + break; + case 100: + standard = "Music"; + break; + case 101: + standard = "Sound effect"; + break; + case 200: + standard = "AE"; + break; + case 300: + standard = "Photo"; + break; + case 301: + standard = "Illustration"; + break; + case 400: + standard = "3D" + } + return standard +} + +function getMediaType(vs) { + var type; + switch (vs) { + case "Music": + case "Sound effect": + case "Photo": + case "Illustration": + case "AE": + type = vs; + break; + default: + type = "Video" + } + return type +} +Number.prototype.formatMoney = function(decPlaces, thouSeparator, decSeparator, currencySymbol) { + decPlaces = isNaN(decPlaces = Math.abs(decPlaces)) ? 2 : decPlaces, decSeparator = null == decSeparator ? "." : decSeparator, thouSeparator = null == thouSeparator ? "," : thouSeparator, currencySymbol = null == currencySymbol ? "$" : currencySymbol; + var n = this, + sign = n < 0 ? "-" : "", + i = parseInt(n = Math.abs(+n || 0).toFixed(decPlaces)) + "", + j = 3 < (j = i.length) ? j % 3 : 0; + return sign + currencySymbol + (j ? i.substr(0, j) + thouSeparator : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thouSeparator) + (decPlaces ? decSeparator + Math.abs(n - i).toFixed(decPlaces).slice(2) : "") + }, + function() { + function Point(x, y) { + this.x = x || 0, this.y = y || 0 + } + Point.prototype.x = null, Point.prototype.y = null, Point.prototype.add = function(v) { + return new Point(this.x + v.x, this.y + v.y) + }, Point.prototype.clone = function() { + return new Point(this.x, this.y) + }, Point.prototype.degreesTo = function(v) { + var dx = this.x - v.x, + dy = this.y - v.y; + return Math.atan2(dy, dx) * (180 / Math.PI) + }, Point.prototype.distance = function(v) { + var x = this.x - v.x, + y = this.y - v.y; + return Math.sqrt(x * x + y * y) + }, Point.prototype.equals = function(toCompare) { + return this.x == toCompare.x && this.y == toCompare.y + }, Point.prototype.interpolate = function(v, f) { + return new Point((this.x + v.x) * f, (this.y + v.y) * f) + }, Point.prototype.length = function() { + return Math.sqrt(this.x * this.x + this.y * this.y) + }, Point.prototype.normalize = function(thickness) { + var l = this.length(); + this.x = this.x / l * thickness, this.y = this.y / l * thickness + }, Point.prototype.orbit = function(origin, arcWidth, arcHeight, degrees) { + var radians = degrees * (Math.PI / 180); + this.x = origin.x + arcWidth * Math.cos(radians), this.y = origin.y + arcHeight * Math.sin(radians) + }, Point.prototype.offset = function(dx, dy) { + this.x += dx, this.y += dy + }, Point.prototype.subtract = function(v) { + return new Point(this.x - v.x, this.y - v.y) + }, Point.prototype.toString = function() { + return "(x=" + this.x + ", y=" + this.y + ")" + }, Point.interpolate = function(pt1, pt2, f) { + return new Point((pt1.x + pt2.x) * f, (pt1.y + pt2.y) * f) + }, Point.polar = function(len, angle) { + return new Point(len * Math.sin(angle), len * Math.cos(angle)) + }, Point.distance = function(pt1, pt2) { + var x = pt1.x - pt2.x, + y = pt1.y - pt2.y; + return Math.sqrt(x * x + y * y) + }, this.Point = window.Point = Point + }(), ExtendedID.extend = function(id) { + if (id) { + for (var extendedID = id.toString(); extendedID.length < 9;) extendedID = "0" + extendedID; + return extendedID + } + }, String.prototype.insert = function(index, string) { + return 0 < index ? this.substring(0, index) + string + this.substring(index, this.length) : string + this + }, String.prototype.replaceAll = function(search, replacement) { + return this.replace(new RegExp(search, "g"), replacement) + }, getMousePosition = function(element) { + for (var xPosition = 0, yPosition = 0; element;) xPosition += element.offsetLeft - element.scrollLeft + element.clientLeft, yPosition += element.offsetTop - element.scrollTop + element.clientTop, element = element.offsetParent; + return { + x: xPosition, + y: yPosition + } + }, getScroll = function() { + if (null != window.pageYOffset) return [pageXOffset, pageYOffset]; + var d = document, + r = d.documentElement, + b = d.body; + return [r.scrollLeft || b.scrollLeft || 0, r.scrollTop || b.scrollTop || 0] + }, getUserHome = function() { + return require("os").homedir() + }, getName = function(input) { + for (; - 1 != input.indexOf(",");) input = input.replace(",", " "); + for (; - 1 != input.indexOf("&");) input = input.replace("&", "and"); + for (; - 1 != input.indexOf("/");) input = input.replace("/", " "); + for (; - 1 != input.indexOf("'");) input = input.replace("'", " "); + for (; - 1 != input.indexOf("(");) input = input.replace("(", " "); + for (; - 1 != input.indexOf(")");) input = input.replace(")", " "); + for (; - 1 != input.indexOf(":");) input = input.replace(":", " "); + return input + }, getPosition = function(element) { + for (var xPosition = 0, yPosition = 0; element;) xPosition += element.offsetLeft - element.scrollLeft + element.clientLeft, yPosition += element.offsetTop - element.scrollTop + element.clientTop, element = element.offsetParent; + return { + x: xPosition, + y: yPosition + } + }, getChromeVersion = function() { + var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + return !!raw && parseInt(raw[2], 10) + }; diff --git a/pype/premiere/static_ppro/js/pico_client.js b/pype/premiere/static_ppro/js/pico_client.js new file mode 100644 index 0000000000..fdb8e31a18 --- /dev/null +++ b/pype/premiere/static_ppro/js/pico_client.js @@ -0,0 +1,75 @@ +// pico connection of python module name +var api = pico.importModule('api'); + +function querySelector(parent) { + return function (child) { + return document.querySelector(parent).querySelector(child) + }; +} + +var defs = {} + +function jumpTo(name) { + var e = defs[name]; + document.querySelectorAll('.highlight').forEach(function (el) { + el.classList.remove('highlight'); + }); + e.classList.add('highlight'); + return false; +} + +function unindent(code) { + var lines = code.split('\n'); + var margin = -1; + for (var j = 0; j < lines.length; j++) { + var l = lines[j]; + for (i = 0; i < l.length; i++) { + if (l[i] != " ") { + margin = i; + break; + } + } + if (margin > -1) { + break; + } + } + lines = lines.slice(j); + return lines.map(function (s) { + return s.substr(margin) + }).join('\n'); +} + + +function ready() { + // // set the element of each example to the corresponding functions source + // document.querySelectorAll('li pre code.js').forEach(function(e){ + // var id = e.parentElement.parentElement.id; + // var f = window[id]; + // var code = f.toString().split('\n').slice(2, -1).join('\n'); + // e.innerText = unindent(code); + // }) + + document.querySelectorAll('li pre code.html').forEach(function (e) { + var html = e.parentElement.parentElement.querySelector('div.example').innerHTML; + e.innerText = unindent(html); + }) + + hljs.initHighlighting(); + + // // find all the elements representing the function definitions in the python source + // document.querySelectorAll('.python .hljs-function .hljs-title').forEach(function(e){ + // var a = document.createElement('a'); + // a.name = e.innerText; + // e.parentElement.insertBefore(a, e) + // return defs[e.innerText] = e.parentElement; + // }); + + // convert all 'api.X' strings to hyperlinks to jump to python source + document.querySelectorAll('.js').forEach(function (e) { + var code = e.innerHTML; + Object.keys(defs).forEach(function (k) { + code = code.replace('api.' + k + '(', '
    api.' + k + '('); + }) + e.innerHTML = code; + }) +} diff --git a/pype/premiere/static_ppro/js/vendor/CSInterface-8.js b/pype/premiere/static_ppro/js/vendor/CSInterface-8.js new file mode 100644 index 0000000000..4239391efd --- /dev/null +++ b/pype/premiere/static_ppro/js/vendor/CSInterface-8.js @@ -0,0 +1,1193 @@ +/************************************************************************************************** +* +* ADOBE SYSTEMS INCORPORATED +* Copyright 2013 Adobe Systems Incorporated +* All Rights Reserved. +* +* NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the +* terms of the Adobe license agreement accompanying it. If you have received this file from a +* source other than Adobe, then your use, modification, or distribution of it requires the prior +* written permission of Adobe. +* +**************************************************************************************************/ + +/** CSInterface - v8.0.0 */ + +/** + * Stores constants for the window types supported by the CSXS infrastructure. + */ +function CSXSWindowType() +{ +} + +/** Constant for the CSXS window type Panel. */ +CSXSWindowType._PANEL = "Panel"; + +/** Constant for the CSXS window type Modeless. */ +CSXSWindowType._MODELESS = "Modeless"; + +/** Constant for the CSXS window type ModalDialog. */ +CSXSWindowType._MODAL_DIALOG = "ModalDialog"; + +/** EvalScript error message */ +EvalScript_ErrMessage = "EvalScript error."; + +/** + * @class Version + * Defines a version number with major, minor, micro, and special + * components. The major, minor and micro values are numeric; the special + * value can be any string. + * + * @param major The major version component, a positive integer up to nine digits long. + * @param minor The minor version component, a positive integer up to nine digits long. + * @param micro The micro version component, a positive integer up to nine digits long. + * @param special The special version component, an arbitrary string. + * + * @return A new \c Version object. + */ +function Version(major, minor, micro, special) +{ + this.major = major; + this.minor = minor; + this.micro = micro; + this.special = special; +} + +/** + * The maximum value allowed for a numeric version component. + * This reflects the maximum value allowed in PlugPlug and the manifest schema. + */ +Version.MAX_NUM = 999999999; + +/** + * @class VersionBound + * Defines a boundary for a version range, which associates a \c Version object + * with a flag for whether it is an inclusive or exclusive boundary. + * + * @param version The \c #Version object. + * @param inclusive True if this boundary is inclusive, false if it is exclusive. + * + * @return A new \c VersionBound object. + */ +function VersionBound(version, inclusive) +{ + this.version = version; + this.inclusive = inclusive; +} + +/** + * @class VersionRange + * Defines a range of versions using a lower boundary and optional upper boundary. + * + * @param lowerBound The \c #VersionBound object. + * @param upperBound The \c #VersionBound object, or null for a range with no upper boundary. + * + * @return A new \c VersionRange object. + */ +function VersionRange(lowerBound, upperBound) +{ + this.lowerBound = lowerBound; + this.upperBound = upperBound; +} + +/** + * @class Runtime + * Represents a runtime related to the CEP infrastructure. + * Extensions can declare dependencies on particular + * CEP runtime versions in the extension manifest. + * + * @param name The runtime name. + * @param version A \c #VersionRange object that defines a range of valid versions. + * + * @return A new \c Runtime object. + */ +function Runtime(name, versionRange) +{ + this.name = name; + this.versionRange = versionRange; +} + +/** +* @class Extension +* Encapsulates a CEP-based extension to an Adobe application. +* +* @param id The unique identifier of this extension. +* @param name The localizable display name of this extension. +* @param mainPath The path of the "index.html" file. +* @param basePath The base path of this extension. +* @param windowType The window type of the main window of this extension. + Valid values are defined by \c #CSXSWindowType. +* @param width The default width in pixels of the main window of this extension. +* @param height The default height in pixels of the main window of this extension. +* @param minWidth The minimum width in pixels of the main window of this extension. +* @param minHeight The minimum height in pixels of the main window of this extension. +* @param maxWidth The maximum width in pixels of the main window of this extension. +* @param maxHeight The maximum height in pixels of the main window of this extension. +* @param defaultExtensionDataXml The extension data contained in the default \c ExtensionDispatchInfo section of the extension manifest. +* @param specialExtensionDataXml The extension data contained in the application-specific \c ExtensionDispatchInfo section of the extension manifest. +* @param requiredRuntimeList An array of \c Runtime objects for runtimes required by this extension. +* @param isAutoVisible True if this extension is visible on loading. +* @param isPluginExtension True if this extension has been deployed in the Plugins folder of the host application. +* +* @return A new \c Extension object. +*/ +function Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight, + defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension) +{ + this.id = id; + this.name = name; + this.mainPath = mainPath; + this.basePath = basePath; + this.windowType = windowType; + this.width = width; + this.height = height; + this.minWidth = minWidth; + this.minHeight = minHeight; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.defaultExtensionDataXml = defaultExtensionDataXml; + this.specialExtensionDataXml = specialExtensionDataXml; + this.requiredRuntimeList = requiredRuntimeList; + this.isAutoVisible = isAutoVisible; + this.isPluginExtension = isPluginExtension; +} + +/** + * @class CSEvent + * A standard JavaScript event, the base class for CEP events. + * + * @param type The name of the event type. + * @param scope The scope of event, can be "GLOBAL" or "APPLICATION". + * @param appId The unique identifier of the application that generated the event. + * @param extensionId The unique identifier of the extension that generated the event. + * + * @return A new \c CSEvent object + */ +function CSEvent(type, scope, appId, extensionId) +{ + this.type = type; + this.scope = scope; + this.appId = appId; + this.extensionId = extensionId; +} + +/** Event-specific data. */ +CSEvent.prototype.data = ""; + +/** + * @class SystemPath + * Stores operating-system-specific location constants for use in the + * \c #CSInterface.getSystemPath() method. + * @return A new \c SystemPath object. + */ +function SystemPath() +{ +} + +/** The path to user data. */ +SystemPath.USER_DATA = "userData"; + +/** The path to common files for Adobe applications. */ +SystemPath.COMMON_FILES = "commonFiles"; + +/** The path to the user's default document folder. */ +SystemPath.MY_DOCUMENTS = "myDocuments"; + +/** @deprecated. Use \c #SystemPath.Extension. */ +SystemPath.APPLICATION = "application"; + +/** The path to current extension. */ +SystemPath.EXTENSION = "extension"; + +/** The path to hosting application's executable. */ +SystemPath.HOST_APPLICATION = "hostApplication"; + +/** + * @class ColorType + * Stores color-type constants. + */ +function ColorType() +{ +} + +/** RGB color type. */ +ColorType.RGB = "rgb"; + +/** Gradient color type. */ +ColorType.GRADIENT = "gradient"; + +/** Null color type. */ +ColorType.NONE = "none"; + +/** + * @class RGBColor + * Stores an RGB color with red, green, blue, and alpha values. + * All values are in the range [0.0 to 255.0]. Invalid numeric values are + * converted to numbers within this range. + * + * @param red The red value, in the range [0.0 to 255.0]. + * @param green The green value, in the range [0.0 to 255.0]. + * @param blue The blue value, in the range [0.0 to 255.0]. + * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0]. + * The default, 255.0, means that the color is fully opaque. + * + * @return A new RGBColor object. + */ +function RGBColor(red, green, blue, alpha) +{ + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; +} + +/** + * @class Direction + * A point value in which the y component is 0 and the x component + * is positive or negative for a right or left direction, + * or the x component is 0 and the y component is positive or negative for + * an up or down direction. + * + * @param x The horizontal component of the point. + * @param y The vertical component of the point. + * + * @return A new \c Direction object. + */ +function Direction(x, y) +{ + this.x = x; + this.y = y; +} + +/** + * @class GradientStop + * Stores gradient stop information. + * + * @param offset The offset of the gradient stop, in the range [0.0 to 1.0]. + * @param rgbColor The color of the gradient at this point, an \c #RGBColor object. + * + * @return GradientStop object. + */ +function GradientStop(offset, rgbColor) +{ + this.offset = offset; + this.rgbColor = rgbColor; +} + +/** + * @class GradientColor + * Stores gradient color information. + * + * @param type The gradient type, must be "linear". + * @param direction A \c #Direction object for the direction of the gradient + (up, down, right, or left). + * @param numStops The number of stops in the gradient. + * @param gradientStopList An array of \c #GradientStop objects. + * + * @return A new \c GradientColor object. + */ +function GradientColor(type, direction, numStops, arrGradientStop) +{ + this.type = type; + this.direction = direction; + this.numStops = numStops; + this.arrGradientStop = arrGradientStop; +} + +/** + * @class UIColor + * Stores color information, including the type, anti-alias level, and specific color + * values in a color object of an appropriate type. + * + * @param type The color type, 1 for "rgb" and 2 for "gradient". + The supplied color object must correspond to this type. + * @param antialiasLevel The anti-alias level constant. + * @param color A \c #RGBColor or \c #GradientColor object containing specific color information. + * + * @return A new \c UIColor object. + */ +function UIColor(type, antialiasLevel, color) +{ + this.type = type; + this.antialiasLevel = antialiasLevel; + this.color = color; +} + +/** + * @class AppSkinInfo + * Stores window-skin properties, such as color and font. All color parameter values are \c #UIColor objects except that systemHighlightColor is \c #RGBColor object. + * + * @param baseFontFamily The base font family of the application. + * @param baseFontSize The base font size of the application. + * @param appBarBackgroundColor The application bar background color. + * @param panelBackgroundColor The background color of the extension panel. + * @param appBarBackgroundColorSRGB The application bar background color, as sRGB. + * @param panelBackgroundColorSRGB The background color of the extension panel, as sRGB. + * @param systemHighlightColor The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color. + * + * @return AppSkinInfo object. + */ +function AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor) +{ + this.baseFontFamily = baseFontFamily; + this.baseFontSize = baseFontSize; + this.appBarBackgroundColor = appBarBackgroundColor; + this.panelBackgroundColor = panelBackgroundColor; + this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB; + this.panelBackgroundColorSRGB = panelBackgroundColorSRGB; + this.systemHighlightColor = systemHighlightColor; +} + +/** + * @class HostEnvironment + * Stores information about the environment in which the extension is loaded. + * + * @param appName The application's name. + * @param appVersion The application's version. + * @param appLocale The application's current license locale. + * @param appUILocale The application's current UI locale. + * @param appId The application's unique identifier. + * @param isAppOnline True if the application is currently online. + * @param appSkinInfo An \c #AppSkinInfo object containing the application's default color and font styles. + * + * @return A new \c HostEnvironment object. + */ +function HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo) +{ + this.appName = appName; + this.appVersion = appVersion; + this.appLocale = appLocale; + this.appUILocale = appUILocale; + this.appId = appId; + this.isAppOnline = isAppOnline; + this.appSkinInfo = appSkinInfo; +} + +/** + * @class HostCapabilities + * Stores information about the host capabilities. + * + * @param EXTENDED_PANEL_MENU True if the application supports panel menu. + * @param EXTENDED_PANEL_ICONS True if the application supports panel icon. + * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine. + * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions. + * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions. + * + * @return A new \c HostCapabilities object. + */ +function HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS) +{ + this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU; + this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS; + this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE; + this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS; + this.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0 +} + +/** + * @class ApiVersion + * Stores current api version. + * + * Since 4.2.0 + * + * @param major The major version + * @param minor The minor version. + * @param micro The micro version. + * + * @return ApiVersion object. + */ +function ApiVersion(major, minor, micro) +{ + this.major = major; + this.minor = minor; + this.micro = micro; +} + +/** + * @class MenuItemStatus + * Stores flyout menu item status + * + * Since 5.2.0 + * + * @param menuItemLabel The menu item label. + * @param enabled True if user wants to enable the menu item. + * @param checked True if user wants to check the menu item. + * + * @return MenuItemStatus object. + */ +function MenuItemStatus(menuItemLabel, enabled, checked) +{ + this.menuItemLabel = menuItemLabel; + this.enabled = enabled; + this.checked = checked; +} + +/** + * @class ContextMenuItemStatus + * Stores the status of the context menu item. + * + * Since 5.2.0 + * + * @param menuItemID The menu item id. + * @param enabled True if user wants to enable the menu item. + * @param checked True if user wants to check the menu item. + * + * @return MenuItemStatus object. + */ +function ContextMenuItemStatus(menuItemID, enabled, checked) +{ + this.menuItemID = menuItemID; + this.enabled = enabled; + this.checked = checked; +} +//------------------------------ CSInterface ---------------------------------- + +/** + * @class CSInterface + * This is the entry point to the CEP extensibility infrastructure. + * Instantiate this object and use it to: + *
      + *
    • Access information about the host application in which an extension is running
    • + *
    • Launch an extension
    • + *
    • Register interest in event notifications, and dispatch events
    • + *
    + * + * @return A new \c CSInterface object + */ +function CSInterface() +{ +} + +/** + * User can add this event listener to handle native application theme color changes. + * Callback function gives extensions ability to fine-tune their theme color after the + * global theme color has been changed. + * The callback function should be like below: + * + * @example + * // event is a CSEvent object, but user can ignore it. + * function OnAppThemeColorChanged(event) + * { + * // Should get a latest HostEnvironment object from application. + * var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo; + * // Gets the style information such as color info from the skinInfo, + * // and redraw all UI controls of your extension according to the style info. + * } + */ +CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged"; + +/** The host environment data object. */ +CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null; + +/** Retrieves information about the host environment in which the + * extension is currently running. + * + * @return A \c #HostEnvironment object. + */ +CSInterface.prototype.getHostEnvironment = function() +{ + this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment()); + return this.hostEnvironment; +}; + +/** Closes this extension. */ +CSInterface.prototype.closeExtension = function() +{ + window.__adobe_cep__.closeExtension(); +}; + +/** + * Retrieves a path for which a constant is defined in the system. + * + * @param pathType The path-type constant defined in \c #SystemPath , + * + * @return The platform-specific system path string. + */ +CSInterface.prototype.getSystemPath = function(pathType) +{ + var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType)); + var OSVersion = this.getOSInformation(); + if (OSVersion.indexOf("Windows") >= 0) + { + path = path.replace("file:///", ""); + } + else if (OSVersion.indexOf("Mac") >= 0) + { + path = path.replace("file://", ""); + } + return path; +}; + +/** + * Evaluates a JavaScript script, which can use the JavaScript DOM + * of the host application. + * + * @param script The JavaScript script. + * @param callback Optional. A callback function that receives the result of execution. + * If execution fails, the callback function receives the error message \c EvalScript_ErrMessage. + */ +CSInterface.prototype.evalScript = function(script, callback) +{ + if(callback === null || callback === undefined) + { + callback = function(result){}; + } + window.__adobe_cep__.evalScript(script, callback); +}; + +/** + * Retrieves the unique identifier of the application. + * in which the extension is currently running. + * + * @return The unique ID string. + */ +CSInterface.prototype.getApplicationID = function() +{ + var appId = this.hostEnvironment.appId; + return appId; +}; + +/** + * Retrieves host capability information for the application + * in which the extension is currently running. + * + * @return A \c #HostCapabilities object. + */ +CSInterface.prototype.getHostCapabilities = function() +{ + var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() ); + return hostCapabilities; +}; + +/** + * Triggers a CEP event programmatically. Yoy can use it to dispatch + * an event of a predefined type, or of a type you have defined. + * + * @param event A \c CSEvent object. + */ +CSInterface.prototype.dispatchEvent = function(event) +{ + if (typeof event.data == "object") + { + event.data = JSON.stringify(event.data); + } + + window.__adobe_cep__.dispatchEvent(event); +}; + +/** + * Registers an interest in a CEP event of a particular type, and + * assigns an event handler. + * The event infrastructure notifies your extension when events of this type occur, + * passing the event object to the registered handler function. + * + * @param type The name of the event type of interest. + * @param listener The JavaScript handler function or method. + * @param obj Optional, the object containing the handler method, if any. + * Default is null. + */ +CSInterface.prototype.addEventListener = function(type, listener, obj) +{ + window.__adobe_cep__.addEventListener(type, listener, obj); +}; + +/** + * Removes a registered event listener. + * + * @param type The name of the event type of interest. + * @param listener The JavaScript handler function or method that was registered. + * @param obj Optional, the object containing the handler method, if any. + * Default is null. + */ +CSInterface.prototype.removeEventListener = function(type, listener, obj) +{ + window.__adobe_cep__.removeEventListener(type, listener, obj); +}; + +/** + * Loads and launches another extension, or activates the extension if it is already loaded. + * + * @param extensionId The extension's unique identifier. + * @param startupParams Not currently used, pass "". + * + * @example + * To launch the extension "help" with ID "HLP" from this extension, call: + * requestOpenExtension("HLP", ""); + * + */ +CSInterface.prototype.requestOpenExtension = function(extensionId, params) +{ + window.__adobe_cep__.requestOpenExtension(extensionId, params); +}; + +/** + * Retrieves the list of extensions currently loaded in the current host application. + * The extension list is initialized once, and remains the same during the lifetime + * of the CEP session. + * + * @param extensionIds Optional, an array of unique identifiers for extensions of interest. + * If omitted, retrieves data for all extensions. + * + * @return Zero or more \c #Extension objects. + */ +CSInterface.prototype.getExtensions = function(extensionIds) +{ + var extensionIdsStr = JSON.stringify(extensionIds); + var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr); + + var extensions = JSON.parse(extensionsStr); + return extensions; +}; + +/** + * Retrieves network-related preferences. + * + * @return A JavaScript object containing network preferences. + */ +CSInterface.prototype.getNetworkPreferences = function() +{ + var result = window.__adobe_cep__.getNetworkPreferences(); + var networkPre = JSON.parse(result); + + return networkPre; +}; + +/** + * Initializes the resource bundle for this extension with property values + * for the current application and locale. + * To support multiple locales, you must define a property file for each locale, + * containing keyed display-string values for that locale. + * See localization documentation for Extension Builder and related products. + * + * Keys can be in the + * form key.value="localized string", for use in HTML text elements. + * For example, in this input element, the localized \c key.value string is displayed + * instead of the empty \c value string: + * + * + * + * @return An object containing the resource bundle information. + */ +CSInterface.prototype.initResourceBundle = function() +{ + var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle()); + var resElms = document.querySelectorAll('[data-locale]'); + for (var n = 0; n < resElms.length; n++) + { + var resEl = resElms[n]; + // Get the resource key from the element. + var resKey = resEl.getAttribute('data-locale'); + if (resKey) + { + // Get all the resources that start with the key. + for (var key in resourceBundle) + { + if (key.indexOf(resKey) === 0) + { + var resValue = resourceBundle[key]; + if (key.length == resKey.length) + { + resEl.innerHTML = resValue; + } + else if ('.' == key.charAt(resKey.length)) + { + var attrKey = key.substring(resKey.length + 1); + resEl[attrKey] = resValue; + } + } + } + } + } + return resourceBundle; +}; + +/** + * Writes installation information to a file. + * + * @return The file path. + */ +CSInterface.prototype.dumpInstallationInfo = function() +{ + return window.__adobe_cep__.dumpInstallationInfo(); +}; + +/** + * Retrieves version information for the current Operating System, + * See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values. + * + * @return A string containing the OS version, or "unknown Operation System". + * If user customizes the User Agent by setting CEF command parameter "--user-agent", only + * "Mac OS X" or "Windows" will be returned. + */ +CSInterface.prototype.getOSInformation = function() +{ + var userAgent = navigator.userAgent; + + if ((navigator.platform == "Win32") || (navigator.platform == "Windows")) + { + var winVersion = "Windows"; + var winBit = ""; + if (userAgent.indexOf("Windows") > -1) + { + if (userAgent.indexOf("Windows NT 5.0") > -1) + { + winVersion = "Windows 2000"; + } + else if (userAgent.indexOf("Windows NT 5.1") > -1) + { + winVersion = "Windows XP"; + } + else if (userAgent.indexOf("Windows NT 5.2") > -1) + { + winVersion = "Windows Server 2003"; + } + else if (userAgent.indexOf("Windows NT 6.0") > -1) + { + winVersion = "Windows Vista"; + } + else if (userAgent.indexOf("Windows NT 6.1") > -1) + { + winVersion = "Windows 7"; + } + else if (userAgent.indexOf("Windows NT 6.2") > -1) + { + winVersion = "Windows 8"; + } + else if (userAgent.indexOf("Windows NT 6.3") > -1) + { + winVersion = "Windows 8.1"; + } + else if (userAgent.indexOf("Windows NT 10") > -1) + { + winVersion = "Windows 10"; + } + + if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1) + { + winBit = " 64-bit"; + } + else + { + winBit = " 32-bit"; + } + } + + return winVersion + winBit; + } + else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh")) + { + var result = "Mac OS X"; + + if (userAgent.indexOf("Mac OS X") > -1) + { + result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")")); + result = result.replace(/_/g, "."); + } + + return result; + } + + return "Unknown Operation System"; +}; + +/** + * Opens a page in the default system browser. + * + * Since 4.2.0 + * + * @param url The URL of the page/file to open, or the email address. + * Must use HTTP/HTTPS/file/mailto protocol. For example: + * "http://www.adobe.com" + * "https://github.com" + * "file:///C:/log.txt" + * "mailto:test@adobe.com" + * + * @return One of these error codes:\n + *
      \n + *
    • NO_ERROR - 0
    • \n + *
    • ERR_UNKNOWN - 1
    • \n + *
    • ERR_INVALID_PARAMS - 2
    • \n + *
    • ERR_INVALID_URL - 201
    • \n + *
    \n + */ +CSInterface.prototype.openURLInDefaultBrowser = function(url) +{ + return cep.util.openURLInDefaultBrowser(url); +}; + +/** + * Retrieves extension ID. + * + * Since 4.2.0 + * + * @return extension ID. + */ +CSInterface.prototype.getExtensionID = function() +{ + return window.__adobe_cep__.getExtensionId(); +}; + +/** + * Retrieves the scale factor of screen. + * On Windows platform, the value of scale factor might be different from operating system's scale factor, + * since host application may use its self-defined scale factor. + * + * Since 4.2.0 + * + * @return One of the following float number. + *
      \n + *
    • -1.0 when error occurs
    • \n + *
    • 1.0 means normal screen
    • \n + *
    • >1.0 means HiDPI screen
    • \n + *
    \n + */ +CSInterface.prototype.getScaleFactor = function() +{ + return window.__adobe_cep__.getScaleFactor(); +}; + +/** + * Set a handler to detect any changes of scale factor. This only works on Mac. + * + * Since 4.2.0 + * + * @param handler The function to be called when scale factor is changed. + * + */ +CSInterface.prototype.setScaleFactorChangedHandler = function(handler) +{ + window.__adobe_cep__.setScaleFactorChangedHandler(handler); +}; + +/** + * Retrieves current API version. + * + * Since 4.2.0 + * + * @return ApiVersion object. + * + */ +CSInterface.prototype.getCurrentApiVersion = function() +{ + var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion()); + return apiVersion; +}; + +/** + * Set panel flyout menu by an XML. + * + * Since 5.2.0 + * + * Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a + * menu item is clicked. + * The "data" attribute of event is an object which contains "menuId" and "menuName" attributes. + * + * Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed" + * respectively to get notified when flyout menu is opened or closed. + * + * @param menu A XML string which describes menu structure. + * An example menu XML: + * + * + * + * + * + * + * + * + * + * + * + * + */ +CSInterface.prototype.setPanelFlyoutMenu = function(menu) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu); +}; + +/** + * Updates a menu item in the extension window's flyout menu, by setting the enabled + * and selection status. + * + * Since 5.2.0 + * + * @param menuItemLabel The menu item label. + * @param enabled True to enable the item, false to disable it (gray it out). + * @param checked True to select the item, false to deselect it. + * + * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). + * Fails silently if menu label is invalid. + * + * @see HostCapabilities.EXTENDED_PANEL_MENU + */ +CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked) +{ + var ret = false; + if (this.getHostCapabilities().EXTENDED_PANEL_MENU) + { + var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked); + ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus)); + } + return ret; +}; + + +/** + * Set context menu by XML string. + * + * Since 5.2.0 + * + * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. + * - an item without menu ID or menu name is disabled and is not shown. + * - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. + * - Checkable attribute takes precedence over Checked attribute. + * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. + The Chrome extension contextMenus API was taken as a reference. + https://developer.chrome.com/extensions/contextMenus + * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. + * + * @param menu A XML string which describes menu structure. + * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. + * + * @description An example menu XML: + * + * + * + * + * + * + * + * + * + * + * + */ +CSInterface.prototype.setContextMenu = function(menu, callback) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback); +}; + +/** + * Set context menu by JSON string. + * + * Since 6.0.0 + * + * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. + * - an item without menu ID or menu name is disabled and is not shown. + * - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. + * - Checkable attribute takes precedence over Checked attribute. + * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. + The Chrome extension contextMenus API was taken as a reference. + * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. + https://developer.chrome.com/extensions/contextMenus + * + * @param menu A JSON string which describes menu structure. + * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. + * + * @description An example menu JSON: + * + * { + * "menu": [ + * { + * "id": "menuItemId1", + * "label": "testExample1", + * "enabled": true, + * "checkable": true, + * "checked": false, + * "icon": "./image/small_16X16.png" + * }, + * { + * "id": "menuItemId2", + * "label": "testExample2", + * "menu": [ + * { + * "id": "menuItemId2-1", + * "label": "testExample2-1", + * "menu": [ + * { + * "id": "menuItemId2-1-1", + * "label": "testExample2-1-1", + * "enabled": false, + * "checkable": true, + * "checked": true + * } + * ] + * }, + * { + * "id": "menuItemId2-2", + * "label": "testExample2-2", + * "enabled": true, + * "checkable": true, + * "checked": true + * } + * ] + * }, + * { + * "label": "---" + * }, + * { + * "id": "menuItemId3", + * "label": "testExample3", + * "enabled": false, + * "checkable": true, + * "checked": false + * } + * ] + * } + * + */ +CSInterface.prototype.setContextMenuByJSON = function(menu, callback) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback); +}; + +/** + * Updates a context menu item by setting the enabled and selection status. + * + * Since 5.2.0 + * + * @param menuItemID The menu item ID. + * @param enabled True to enable the item, false to disable it (gray it out). + * @param checked True to select the item, false to deselect it. + */ +CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked) +{ + var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked); + ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus)); +}; + +/** + * Get the visibility status of an extension window. + * + * Since 6.0.0 + * + * @return true if the extension window is visible; false if the extension window is hidden. + */ +CSInterface.prototype.isWindowVisible = function() +{ + return window.__adobe_cep__.invokeSync("isWindowVisible", ""); +}; + +/** + * Resize extension's content to the specified dimensions. + * 1. Works with modal and modeless extensions in all Adobe products. + * 2. Extension's manifest min/max size constraints apply and take precedence. + * 3. For panel extensions + * 3.1 This works in all Adobe products except: + * * Premiere Pro + * * Prelude + * * After Effects + * 3.2 When the panel is in certain states (especially when being docked), + * it will not change to the desired dimensions even when the + * specified size satisfies min/max constraints. + * + * Since 6.0.0 + * + * @param width The new width + * @param height The new height + */ +CSInterface.prototype.resizeContent = function(width, height) +{ + window.__adobe_cep__.resizeContent(width, height); +}; + +/** + * Register the invalid certificate callback for an extension. + * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame. + * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown. + * + * Since 6.1.0 + * + * @param callback the callback function + */ +CSInterface.prototype.registerInvalidCertificateCallback = function(callback) +{ + return window.__adobe_cep__.registerInvalidCertificateCallback(callback); +}; + +/** + * Register an interest in some key events to prevent them from being sent to the host application. + * + * This function works with modeless extensions and panel extensions. + * Generally all the key events will be sent to the host application for these two extensions if the current focused element + * is not text input or dropdown, + * If you want to intercept some key events and want them to be handled in the extension, please call this function + * in advance to prevent them being sent to the host application. + * + * Since 6.1.0 + * + * @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or + an empty string will lead to removing the interest + * + * This JSON string should be an array, each object has following keys: + * + * keyCode: [Required] represents an OS system dependent virtual key code identifying + * the unmodified value of the pressed key. + * ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred. + * altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred. + * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred. + * metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred. + * On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead. + * An example JSON string: + * + * [ + * { + * "keyCode": 48 + * }, + * { + * "keyCode": 123, + * "ctrlKey": true + * }, + * { + * "keyCode": 123, + * "ctrlKey": true, + * "metaKey": true + * } + * ] + * + */ +CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest) +{ + return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest); +}; + +/** + * Set the title of the extension window. + * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. + * + * Since 6.1.0 + * + * @param title The window title. + */ +CSInterface.prototype.setWindowTitle = function(title) +{ + window.__adobe_cep__.invokeSync("setWindowTitle", title); +}; + +/** + * Get the title of the extension window. + * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. + * + * Since 6.1.0 + * + * @return The window title. + */ +CSInterface.prototype.getWindowTitle = function() +{ + return window.__adobe_cep__.invokeSync("getWindowTitle", ""); +}; diff --git a/pype/premiere/static_ppro/js/vendor/bootstrap.min.js b/pype/premiere/static_ppro/js/vendor/bootstrap.min.js new file mode 100644 index 0000000000..9df6b6c2ce --- /dev/null +++ b/pype/premiere/static_ppro/js/vendor/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.2.1 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("popper.js"),require("jquery")):"function"==typeof define&&define.amd?define(["exports","popper.js","jquery"],e):e(t.bootstrap={},t.Popper,t.jQuery)}(this,function(t,u,g){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
    ',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},De="show",we="out",Ae={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:"show"+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Ne="fade",Oe="show",ke=".tooltip-inner",Pe=".arrow",Le="hover",je="focus",He="click",Re="manual",Ue=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Oe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(Ne);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:Pe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Oe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(g(this.tip).hasClass(Ne)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Oe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[He]=!1,this._activeTrigger[je]=!1,this._activeTrigger[Le]=!1,g(this.tip).hasClass(Ne)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ce+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(ke)),this.getTitle()),g(t).removeClass(Ne+" "+Oe)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return be[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Re){var e=t===Le?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Le?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?je:Le]=!0),g(e.getTipElement()).hasClass(Oe)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?je:Le]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,g(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(pe,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Te);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(Ne),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(ve),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(ve,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.2.1"}},{key:"Default",get:function(){return Ie}},{key:"NAME",get:function(){return pe}},{key:"DATA_KEY",get:function(){return ve}},{key:"Event",get:function(){return Ae}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}();g.fn[pe]=Ue._jQueryInterface,g.fn[pe].Constructor=Ue,g.fn[pe].noConflict=function(){return g.fn[pe]=ye,Ue._jQueryInterface};var We="popover",xe="bs.popover",Fe="."+xe,qe=g.fn[We],Me="bs-popover",Ke=new RegExp("(^|\\s)"+Me+"\\S+","g"),Qe=l({},Ue.Default,{placement:"right",trigger:"click",content:"",template:''}),Be=l({},Ue.DefaultType,{content:"(string|element|function)"}),Ve="fade",Ye="show",Xe=".popover-header",ze=".popover-body",Ge={HIDE:"hide"+Fe,HIDDEN:"hidden"+Fe,SHOW:"show"+Fe,SHOWN:"shown"+Fe,INSERTED:"inserted"+Fe,CLICK:"click"+Fe,FOCUSIN:"focusin"+Fe,FOCUSOUT:"focusout"+Fe,MOUSEENTER:"mouseenter"+Fe,MOUSELEAVE:"mouseleave"+Fe},Je=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Me+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(Xe),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ze),e),t.removeClass(Ve+" "+Ye)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ke);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n}\n\nfunction setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst Util = {\n\n TRANSITION_END: 'bsTransitionEnd',\n\n getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID) // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix))\n return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n\n if (!selector || selector === '#') {\n const hrefAttr = element.getAttribute('href')\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''\n }\n\n return selector && document.querySelector(selector) ? selector : null\n },\n\n getTransitionDurationFromElement(element) {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let transitionDuration = $(element).css('transition-duration')\n let transitionDelay = $(element).css('transition-delay')\n\n const floatTransitionDuration = parseFloat(transitionDuration)\n const floatTransitionDelay = parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END)\n },\n\n // TODO: Remove in v5\n supportsTransitionEnd() {\n return Boolean(TRANSITION_END)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n },\n\n findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return Util.findShadowRoot(element.parentNode)\n }\n}\n\nsetTransitionEndSupport()\n\nexport default Util\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Selector = {\n DISMISS : '[data-dismiss=\"alert\"]'\n}\n\nconst Event = {\n CLOSE : `close${EVENT_KEY}`,\n CLOSED : `closed${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n ALERT : 'alert',\n FADE : 'fade',\n SHOW : 'show'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n let rootElement = this._element\n if (element) {\n rootElement = this._getRootElement(element)\n }\n\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n if (!parent) {\n parent = $(element).closest(`.${ClassName.ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(Event.CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(ClassName.SHOW)\n\n if (!$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(element)\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(transitionDuration)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(Event.CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n $element.data(DATA_KEY, data)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(\n Event.CLICK_DATA_API,\n Selector.DISMISS,\n Alert._handleDismiss(new Alert())\n)\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Alert._jQueryInterface\n$.fn[NAME].Constructor = Alert\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n}\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst ClassName = {\n ACTIVE : 'active',\n BUTTON : 'btn',\n FOCUS : 'focus'\n}\n\nconst Selector = {\n DATA_TOGGLE_CARROT : '[data-toggle^=\"button\"]',\n DATA_TOGGLE : '[data-toggle=\"buttons\"]',\n INPUT : 'input:not([type=\"hidden\"])',\n ACTIVE : '.active',\n BUTTON : '.btn'\n}\n\nconst Event = {\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n Selector.DATA_TOGGLE\n )[0]\n\n if (rootElement) {\n const input = this._element.querySelector(Selector.INPUT)\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = rootElement.querySelector(Selector.ACTIVE)\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName.ACTIVE)\n }\n }\n }\n\n if (triggerChangeEvent) {\n if (input.hasAttribute('disabled') ||\n rootElement.hasAttribute('disabled') ||\n input.classList.contains('disabled') ||\n rootElement.classList.contains('disabled')) {\n return\n }\n input.checked = !this._element.classList.contains(ClassName.ACTIVE)\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !this._element.classList.contains(ClassName.ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName.ACTIVE)\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n event.preventDefault()\n\n let button = event.target\n\n if (!$(button).hasClass(ClassName.BUTTON)) {\n button = $(button).closest(Selector.BUTTON)\n }\n\n Button._jQueryInterface.call($(button), 'toggle')\n })\n .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(Selector.BUTTON)[0]\n $(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Button._jQueryInterface\n$.fn[NAME].Constructor = Button\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n}\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\nconst ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true,\n touch : true\n}\n\nconst DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean',\n touch : 'boolean'\n}\n\nconst Direction = {\n NEXT : 'next',\n PREV : 'prev',\n LEFT : 'left',\n RIGHT : 'right'\n}\n\nconst Event = {\n SLIDE : `slide${EVENT_KEY}`,\n SLID : `slid${EVENT_KEY}`,\n KEYDOWN : `keydown${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`,\n TOUCHSTART : `touchstart${EVENT_KEY}`,\n TOUCHMOVE : `touchmove${EVENT_KEY}`,\n TOUCHEND : `touchend${EVENT_KEY}`,\n POINTERDOWN : `pointerdown${EVENT_KEY}`,\n POINTERUP : `pointerup${EVENT_KEY}`,\n DRAG_START : `dragstart${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n CAROUSEL : 'carousel',\n ACTIVE : 'active',\n SLIDE : 'slide',\n RIGHT : 'carousel-item-right',\n LEFT : 'carousel-item-left',\n NEXT : 'carousel-item-next',\n PREV : 'carousel-item-prev',\n ITEM : 'carousel-item',\n POINTER_EVENT : 'pointer-event'\n}\n\nconst Selector = {\n ACTIVE : '.active',\n ACTIVE_ITEM : '.active.carousel-item',\n ITEM : '.carousel-item',\n ITEM_IMG : '.carousel-item img',\n NEXT_PREV : '.carousel-item-next, .carousel-item-prev',\n INDICATORS : '.carousel-indicators',\n DATA_SLIDE : '[data-slide], [data-slide-to]',\n DATA_RIDE : '[data-ride=\"carousel\"]'\n}\n\nconst PointerType = {\n TOUCH : 'touch',\n PEN : 'pen'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel {\n constructor(element, config) {\n this._items = null\n this._interval = null\n this._activeElement = null\n this._isPaused = false\n this._isSliding = false\n this.touchTimeout = null\n this.touchStartX = 0\n this.touchDeltaX = 0\n\n this._config = this._getConfig(config)\n this._element = element\n this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (this._element.querySelector(Selector.NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n $(this._element).one(Event.SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? Direction.NEXT\n : Direction.PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\n this._items = null\n this._config = null\n this._element = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n // swipe left\n if (direction > 0) {\n this.prev()\n }\n\n // swipe right\n if (direction < 0) {\n this.next()\n }\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element)\n .on(Event.KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(Event.MOUSEENTER, (event) => this.pause(event))\n .on(Event.MOUSELEAVE, (event) => this.cycle(event))\n }\n\n this._addTouchEventListeners()\n }\n\n _addTouchEventListeners() {\n if (!this._touchSupported) {\n return\n }\n\n const start = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchStartX = event.originalEvent.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.originalEvent.touches[0].clientX\n }\n }\n\n const move = (event) => {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n this.touchDeltaX = 0\n } else {\n this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX\n }\n }\n\n const end = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchDeltaX = event.originalEvent.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())\n if (this._pointerEvent) {\n $(this._element).on(Event.POINTERDOWN, (event) => start(event))\n $(this._element).on(Event.POINTERUP, (event) => end(event))\n\n this._element.classList.add(ClassName.POINTER_EVENT)\n } else {\n $(this._element).on(Event.TOUCHSTART, (event) => start(event))\n $(this._element).on(Event.TOUCHMOVE, (event) => move(event))\n $(this._element).on(Event.TOUCHEND, (event) => end(event))\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault()\n this.prev()\n break\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode\n ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))\n : []\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === Direction.NEXT\n const isPrevDirection = direction === Direction.PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === Direction.PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))\n const slideEvent = $.Event(Event.SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))\n $(indicators)\n .removeClass(ClassName.ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName.ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName.LEFT\n orderClassName = ClassName.NEXT\n eventDirectionName = Direction.LEFT\n } else {\n directionalClassName = ClassName.RIGHT\n orderClassName = ClassName.PREV\n eventDirectionName = Direction.RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.isDefaultPrevented()) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n\n const slidEvent = $.Event(Event.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if ($(this._element).hasClass(ClassName.SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)\n if (nextElementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval\n this._config.interval = nextElementInterval\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(activeElement)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(ClassName.ACTIVE)\n\n $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(transitionDuration)\n } else {\n $(activeElement).removeClass(ClassName.ACTIVE)\n $(nextElement).addClass(ClassName.ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n data[action]()\n } else if (_config.interval) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const slideIndex = this.getAttribute('data-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel._jQueryInterface.call($(target), config)\n\n if (slideIndex) {\n $(target).data(DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)\n\n$(window).on(Event.LOAD_DATA_API, () => {\n const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))\n for (let i = 0, len = carousels.length; i < len; i++) {\n const $carousel = $(carousels[i])\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Carousel._jQueryInterface\n$.fn[NAME].Constructor = Carousel\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n}\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n toggle : true,\n parent : ''\n}\n\nconst DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n}\n\nconst Event = {\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SHOW : 'show',\n COLLAPSE : 'collapse',\n COLLAPSING : 'collapsing',\n COLLAPSED : 'collapsed'\n}\n\nconst Dimension = {\n WIDTH : 'width',\n HEIGHT : 'height'\n}\n\nconst Selector = {\n ACTIVES : '.show, .collapsing',\n DATA_TOGGLE : '[data-toggle=\"collapse\"]'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = [].slice.call(document.querySelectorAll(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n\n const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = Util.getSelectorFromElement(elem)\n const filterElement = [].slice.call(document.querySelectorAll(selector))\n .filter((foundElem) => foundElem === element)\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(ClassName.SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))\n .filter((elem) => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === this._config.parent\n }\n\n return elem.classList.contains(ClassName.COLLAPSE)\n })\n\n if (actives.length === 0) {\n actives = null\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(Event.SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(ClassName.COLLAPSE)\n .addClass(ClassName.COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n $(this._triggerArray)\n .removeClass(ClassName.COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .addClass(ClassName.SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(Event.SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n const startEvent = $.Event(Event.HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(ClassName.COLLAPSING)\n .removeClass(ClassName.COLLAPSE)\n .removeClass(ClassName.SHOW)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n\n if (selector !== null) {\n const $elem = $([].slice.call(document.querySelectorAll(selector)))\n if (!$elem.hasClass(ClassName.SHOW)) {\n $(trigger).addClass(ClassName.COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .trigger(Event.HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(Dimension.WIDTH)\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT\n }\n\n _getParent() {\n let parent\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0]\n }\n } else {\n parent = document.querySelector(this._config.parent)\n }\n\n const selector =\n `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n\n const children = [].slice.call(parent.querySelectorAll(selector))\n $(children).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n const isOpen = $(element).hasClass(ClassName.SHOW)\n\n if (triggerArray.length) {\n $(triggerArray)\n .toggleClass(ClassName.COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? document.querySelector(selector) : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault()\n }\n\n const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n const selectors = [].slice.call(document.querySelectorAll(selector))\n\n $(selectors).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Collapse._jQueryInterface\n$.fn[NAME].Constructor = Collapse\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n}\n\nexport default Collapse\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\nconst SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\nconst TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\nconst ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\nconst ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\nconst RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,\n KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DISABLED : 'disabled',\n SHOW : 'show',\n DROPUP : 'dropup',\n DROPRIGHT : 'dropright',\n DROPLEFT : 'dropleft',\n MENURIGHT : 'dropdown-menu-right',\n MENULEFT : 'dropdown-menu-left',\n POSITION_STATIC : 'position-static'\n}\n\nconst Selector = {\n DATA_TOGGLE : '[data-toggle=\"dropdown\"]',\n FORM_CHILD : '.dropdown form',\n MENU : '.dropdown-menu',\n NAVBAR_NAV : '.navbar-nav',\n VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n}\n\nconst AttachmentMap = {\n TOP : 'top-start',\n TOPEND : 'top-end',\n BOTTOM : 'bottom-start',\n BOTTOMEND : 'bottom-end',\n RIGHT : 'right-start',\n RIGHTEND : 'right-end',\n LEFT : 'left-start',\n LEFTEND : 'left-end'\n}\n\nconst Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent',\n reference : 'toggle',\n display : 'dynamic'\n}\n\nconst DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)',\n reference : '(string|element)',\n display : 'string'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown {\n constructor(element, config) {\n this._element = element\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this._element)\n const isActive = $(this._menu).hasClass(ClassName.SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = parent\n } else if (Util.isElement(this._config.reference)) {\n referenceElement = this._config.reference\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n }\n\n // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName.POSITION_STATIC)\n }\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n $(parent).closest(Selector.NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n show() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n hide() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(Event.CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n\n if (parent) {\n this._menu = parent.querySelector(Selector.MENU)\n }\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element.parentNode)\n let placement = AttachmentMap.BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(ClassName.DROPUP)) {\n placement = AttachmentMap.TOP\n if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.TOPEND\n }\n } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT\n } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {\n placement = AttachmentMap.LEFT\n } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getPopperConfig() {\n const offsetConf = {}\n if (typeof this._config.offset === 'function') {\n offsetConf.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets) || {}\n }\n return data\n }\n } else {\n offsetConf.offset = this._config.offset\n }\n\n const popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: offsetConf,\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n }\n }\n\n // Disable Popper.js if we have a static display\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n }\n }\n return popperConfig\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!$(parent).hasClass(ClassName.SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n $(dropdownMenu).removeClass(ClassName.SHOW)\n $(parent)\n .removeClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(ClassName.SHOW)\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n const toggle = parent.querySelector(Selector.DATA_TOGGLE)\n $(toggle).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)\n .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Dropdown._jQueryInterface\n$.fn[NAME].Constructor = Dropdown\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n}\n\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\nconst Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n}\n\nconst DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n RESIZE : `resize${EVENT_KEY}`,\n CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,\n KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,\n MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,\n MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SCROLLBAR_MEASURER : 'modal-scrollbar-measure',\n BACKDROP : 'modal-backdrop',\n OPEN : 'modal-open',\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n DIALOG : '.modal-dialog',\n DATA_TOGGLE : '[data-toggle=\"modal\"]',\n DATA_DISMISS : '[data-dismiss=\"modal\"]',\n FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT : '.sticky-top'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = element.querySelector(Selector.DIALOG)\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._isTransitioning = false\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(Event.SHOW, {\n relatedTarget\n })\n\n $(this._element).trigger(showEvent)\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n Event.CLICK_DISMISS,\n Selector.DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {\n $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = $.Event(Event.HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(Event.FOCUSIN)\n\n $(this._element).removeClass(ClassName.SHOW)\n\n $(this._element).off(Event.CLICK_DISMISS)\n $(this._dialog).off(Event.MOUSEDOWN_DISMISS)\n\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))\n\n /**\n * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `Event.CLICK_DATA_API` event that should remain\n */\n $(document).off(Event.FOCUSIN)\n\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._element = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._isTransitioning = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _showElement(relatedTarget) {\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (!this._element.parentNode ||\n this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.scrollTop = 0\n\n if (transition) {\n Util.reflow(this._element)\n }\n\n $(this._element).addClass(ClassName.SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(Event.SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)\n\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(Event.FOCUSIN) // Guard against infinite focus loop\n .on(Event.FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {\n if (event.which === ESCAPE_KEYCODE) {\n event.preventDefault()\n this.hide()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(Event.KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(Event.RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(ClassName.OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(Event.HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(ClassName.FADE)\n ? ClassName.FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = ClassName.BACKDROP\n\n if (animate) {\n this._backdrop.classList.add(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(Event.CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n if (this._config.backdrop === 'static') {\n this._element.focus()\n } else {\n this.hide()\n }\n })\n\n if (animate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(ClassName.SHOW)\n\n if (!callback) {\n return\n }\n\n if (!animate) {\n callback()\n return\n }\n\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName.SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing =\n this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))\n\n // Adjust fixed content padding\n $(fixedContent).each((index, element) => {\n const actualPadding = element.style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(stickyContent).each((index, element) => {\n const actualMargin = element.style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element)\n .data('margin-right', actualMargin)\n .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $(document.body).css('padding-right')\n $(document.body)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n\n $(document.body).addClass(ClassName.OPEN)\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n $(fixedContent).each((index, element) => {\n const padding = $(element).data('padding-right')\n $(element).removeData('padding-right')\n element.style.paddingRight = padding ? padding : ''\n })\n\n // Restore sticky content\n const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))\n $(elements).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $(document.body).data('padding-right')\n $(document.body).removeData('padding-right')\n document.body.style.paddingRight = padding ? padding : ''\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = ClassName.SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Default,\n ...$(this).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(Event.SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(Event.HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Modal._jQueryInterface\n$.fn[NAME].Constructor = Modal\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n}\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst DefaultType = {\n animation : 'boolean',\n template : 'string',\n title : '(string|element|function)',\n trigger : 'string',\n delay : '(number|object)',\n html : 'boolean',\n selector : '(string|boolean)',\n placement : '(string|function)',\n offset : '(number|string)',\n container : '(string|element|boolean)',\n fallbackPlacement : '(string|array)',\n boundary : '(string|element)'\n}\n\nconst AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n}\n\nconst Default = {\n animation : true,\n template : '
    ' +\n '
    ' +\n '
    ',\n trigger : 'hover focus',\n title : '',\n delay : 0,\n html : false,\n selector : false,\n placement : 'top',\n offset : 0,\n container : false,\n fallbackPlacement : 'flip',\n boundary : 'scrollParent'\n}\n\nconst HoverState = {\n SHOW : 'show',\n OUT : 'out'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TOOLTIP : '.tooltip',\n TOOLTIP_INNER : '.tooltip-inner',\n ARROW : '.arrow'\n}\n\nconst Trigger = {\n HOVER : 'hover',\n FOCUS : 'focus',\n CLICK : 'click',\n MANUAL : 'manual'\n}\n\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip {\n constructor(element, config) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal')\n\n if (this.tip) {\n $(this.tip).remove()\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.element = null\n this.config = null\n this.tip = null\n }\n\n show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const shadowRoot = Util.findShadowRoot(this.element)\n const isInTheDom = $.contains(\n shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(ClassName.FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this._getContainer()\n $(tip).data(this.constructor.DATA_KEY, this)\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container)\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED)\n\n this._popper = new Popper(this.element, tip, {\n placement: attachment,\n modifiers: {\n offset: {\n offset: this.config.offset\n },\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => this._handlePopperPlacementChange(data)\n })\n\n $(tip).addClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HoverState.OUT) {\n this._leave(null, this)\n }\n }\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(this.tip)\n\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n if (callback) {\n callback()\n }\n }\n\n $(this.element).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(tip).removeClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n this._activeTrigger[Trigger.CLICK] = false\n this._activeTrigger[Trigger.FOCUS] = false\n this._activeTrigger[Trigger.HOVER] = false\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(tip)\n\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())\n $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n setElementContent($element, content) {\n const html = this.config.html\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content)\n }\n } else {\n $element.text($(content).text())\n }\n } else {\n $element[html ? 'html' : 'text'](content)\n }\n }\n\n getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getContainer() {\n if (this.config.container === false) {\n return document.body\n }\n\n if (Util.isElement(this.config.container)) {\n return $(this.config.container)\n }\n\n return $(document).find(this.config.container)\n }\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== Trigger.MANUAL) {\n const eventIn = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(\n eventIn,\n this.config.selector,\n (event) => this._enter(event)\n )\n .on(\n eventOut,\n this.config.selector,\n (event) => this._leave(event)\n )\n }\n })\n\n $(this.element).closest('.modal').on(\n 'hide.bs.modal',\n () => {\n if (this.element) {\n this.hide()\n }\n }\n )\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this.element).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n const popperInstance = popperData.instance\n this.tip = popperInstance.popper\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(popperData.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n\n $(tip).removeClass(ClassName.FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tooltip._jQueryInterface\n$.fn[NAME].Constructor = Tooltip\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n}\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '
    ' +\n '
    ' +\n '

    ' +\n '
    '\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TITLE : '.popover-header',\n CONTENT : '.popover-body'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(Selector.TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(Selector.CONTENT), content)\n\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Popover._jQueryInterface\n$.fn[NAME].Constructor = Popover\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n}\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n offset : 10,\n method : 'auto',\n target : ''\n}\n\nconst DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n}\n\nconst Event = {\n ACTIVATE : `activate${EVENT_KEY}`,\n SCROLL : `scroll${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DROPDOWN_ITEM : 'dropdown-item',\n DROPDOWN_MENU : 'dropdown-menu',\n ACTIVE : 'active'\n}\n\nconst Selector = {\n DATA_SPY : '[data-spy=\"scroll\"]',\n ACTIVE : '.active',\n NAV_LIST_GROUP : '.nav, .list-group',\n NAV_LINKS : '.nav-link',\n NAV_ITEMS : '.nav-item',\n LIST_ITEMS : '.list-group-item',\n DROPDOWN : '.dropdown',\n DROPDOWN_ITEMS : '.dropdown-item',\n DROPDOWN_TOGGLE : '.dropdown-toggle'\n}\n\nconst OffsetMethod = {\n OFFSET : 'offset',\n POSITION : 'position'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy {\n constructor(element, config) {\n this._element = element\n this._scrollElement = element.tagName === 'BODY' ? window : element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${Selector.NAV_LINKS},` +\n `${this._config.target} ${Selector.LIST_ITEMS},` +\n `${this._config.target} ${Selector.DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? OffsetMethod.OFFSET : OffsetMethod.POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === OffsetMethod.POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = [].slice.call(document.querySelectorAll(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = document.querySelector(targetSelector)\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\n this._element = null\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.target !== 'string') {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset +\n scrollHeight -\n this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n const offsetLength = this._offsets.length\n for (let i = offsetLength; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector\n .split(',')\n .map((selector) => `${selector}[data-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))\n\n if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {\n $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)\n $link.addClass(ClassName.ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(ClassName.ACTIVE)\n // Set triggered links parents as active\n // With both
      and
    - -
    - -
    - -
    -
    Output
    -
    - -
    - - - - - - \ No newline at end of file diff --git a/pype/premiere/extensions/com.pype.avalon/ppro/js/avalon.js b/pype/premiere/extensions/com.pype.avalon/ppro/js/avalon.js deleted file mode 100644 index 6400c2f964..0000000000 --- a/pype/premiere/extensions/com.pype.avalon/ppro/js/avalon.js +++ /dev/null @@ -1,367 +0,0 @@ -/* global CSInterface, $, querySelector, api, displayResult */ -var csi = new CSInterface(); -var output = document.getElementById('output'); - -var rootFolderPath = csi.getSystemPath(SystemPath.EXTENSION); -var timecodes = cep_node.require('node-timecodes'); -var process = cep_node.require('process'); - - -function getEnv() { - csi.evalScript('pype.getProjectFileData();', function (result) { - process.env.EXTENSION_PATH = rootFolderPath - window.ENV = process.env; - var resultData = JSON.parse(result); - for (key in resultData) { - window.ENV[key] = resultData[key]; - }; - csi.evalScript('pype.setEnvs(' + JSON.stringify(window.ENV) + ')'); - }); -} - -function renderClips() { - csi.evalScript('pype.transcodeExternal(' + rootFolderPath + ');', function (result) { - displayResult(result); - }); -} - -function displayResult(r) { - console.log(r); - csi.evalScript('$.writeln( ' + JSON.stringify(r) + ' )'); - output.classList.remove("error"); - output.innerText = r; -} - -function displayError(e) { - output.classList.add("error"); - output.innerText = e.message; -} - -function loadJSX() { - // get the appName of the currently used app. For Premiere Pro it's "PPRO" - var appName = csi.hostEnvironment.appName; - var extensionPath = csi.getSystemPath(SystemPath.EXTENSION); - - // load general JSX script independent of appName - var extensionRootGeneral = extensionPath + '/jsx/'; - csi.evalScript('$._ext.evalFiles("' + extensionRootGeneral + '")'); - - // load JSX scripts based on appName - var extensionRootApp = extensionPath + '/jsx/' + appName + '/'; - csi.evalScript('$._ext.evalFiles("' + extensionRootApp + '")'); - // csi.evalScript('$._PPP_.logConsoleOutput()'); - getEnv(); - - csi.evalScript('$._PPP_.updateEventPanel( "' + "all plugins are loaded" + '" )'); - csi.evalScript('$._PPP_.updateEventPanel( "' + "testing function done" + '" )'); - -} - -// run all at loading -loadJSX() - - -function loadAnimationRendersToTimeline() { - // it will get type of asset and extension from input - // and start loading script from jsx - var $ = querySelector('#load'); - var data = {}; - data.subset = $('input[name=type]').value; - data.subsetExt = $('input[name=ext]').value; - var requestList = []; - // get all selected clips - csi.evalScript('pype.getClipsForLoadingSubsets( "' + data.subset + '" )', function (result) { - // TODO: need to check if the clips are already created and this is just updating to last versions - var resultObj = JSON.parse(result); - var instances = resultObj[0]; - var numTracks = resultObj[1]; - - var key = ''; - // creating requesting list of dictionaries - for (key in instances) { - var clipData = {}; - clipData.parentClip = instances[key]; - clipData.asset = key; - clipData.subset = data.subset; - clipData.representation = data.subsetExt; - requestList.push(clipData); - } - // gets data from mongodb - api.load_representations(window.ENV['AVALON_PROJECT'], requestList).then( - function (avalonData) { - // creates or updates data on timeline - var makeData = {}; - makeData.binHierarchy = data.subset + '/' + data.subsetExt; - makeData.clips = avalonData; - makeData.numTracks = numTracks; - csi.evalScript('pype.importFiles( ' + JSON.stringify(makeData) + ' )'); - } - ); - }); -} - -function evalScript(script) { - var callback = function (result) { - displayResult(result); - }; - csi.evalScript(script, callback); -} - -function deregister() { - api.deregister_plugin_path().then(displayResult); -} - -function register() { - var $ = querySelector('#register'); - var path = $('input[name=path]').value; - api.register_plugin_path(path).then(displayResult); -} - -function getStagingDir() { - // create stagingDir - const fs = require('fs-extra'); - const os = require('os'); - const path = require('path'); - const UUID = require('pure-uuid'); - const id = new UUID(4).format(); - const stagingDir = path.join(os.tmpdir(), id); - - fs.mkdirs(stagingDir); - return stagingDir; - -} - -function convertPathString(path) { - return path.replace( - new RegExp('\\\\', 'g'), '/').replace(new RegExp('//\\?/', 'g'), ''); -} - -function publish() { - var $ = querySelector('#publish'); - // var gui = $('input[name=gui]').checked; - var gui = true; - var versionUp = $('input[name=version-up]').checked; - var audioOnly = $('input[name=audio-only]').checked; - var jsonSendPath = $('input[name=send-path]').value; - var jsonGetPath = $('input[name=get-path]').value; - var publish_path = window.ENV['PUBLISH_PATH']; - - if (jsonSendPath == '') { - // create temp staging directory on local - var stagingDir = convertPathString(getStagingDir()); - - // copy project file to stagingDir - const fs = require('fs-extra'); - const path = require('path'); - - csi.evalScript('pype.getProjectFileData();', function (result) { - displayResult(result); - var data = JSON.parse(result); - displayResult(stagingDir); - displayResult(data.projectfile); - var destination = convertPathString(path.join(stagingDir, data.projectfile)); - displayResult('copy project file'); - displayResult(data.projectfile); - displayResult(destination); - fs.copyFile(data.projectpath, destination); - displayResult('project file coppied!'); - }); - - // publishing file - csi.evalScript('pype.getPyblishRequest("' + stagingDir + '", ' + audioOnly + ');', function (r) { - var request = JSON.parse(r); - displayResult(JSON.stringify(request)); - - csi.evalScript('pype.encodeRepresentation(' + JSON.stringify(request) + ');', function (result) { - // create json for pyblish - var jsonfile = require('jsonfile'); - var jsonSendPath = stagingDir + '_send.json' - var jsonGetPath = stagingDir + '_get.json' - $('input[name=send-path]').value = jsonSendPath; - $('input[name=get-path]').value = jsonGetPath; - var jsonContent = JSON.parse(result); - jsonfile.writeFile(jsonSendPath, jsonContent); - var checkingFile = function (path) { - var timeout = 1000; - setTimeout(function () { - if (fs.existsSync(path)) { - // register publish path - api.register_plugin_path(publish_path).then(displayResult); - // send json to pyblish - api.publish(jsonSendPath, jsonGetPath, gui).then(function (result) { - // check if resulted path exists as file - if (fs.existsSync(result.get_json_path)) { - // read json data from resulted path - displayResult('Updating metadata of clips after publishing'); - - jsonfile.readFile(result.get_json_path, function (err, json) { - csi.evalScript('pype.dumpPublishedInstancesToMetadata(' + JSON.stringify(json) + ');'); - }) - - // version up project - if (versionUp) { - displayResult('Saving new version of the project file'); - csi.evalScript('pype.versionUpWorkFile();'); - }; - } else { - // if resulted path file not existing - displayResult('Publish has not been finished correctly. Hit Publish again to publish from already rendered data, or Reset to render all again.'); - }; - - }); - - } else { - displayResult('waiting'); - checkingFile(path); - }; - }, - timeout) - }; - - checkingFile(jsonContent.waitingFor) - }); - }); - } else { - // register publish path - api.register_plugin_path(publish_path).then(displayResult); - // send json to pyblish - api.publish(jsonSendPath, jsonGetPath, gui).then(function (result) { - // check if resulted path exists as file - if (fs.existsSync(result.get_json_path)) { - // read json data from resulted path - displayResult('Updating metadata of clips after publishing'); - - jsonfile.readFile(result.get_json_path, function (err, json) { - csi.evalScript('pype.dumpPublishedInstancesToMetadata(' + JSON.stringify(json) + ');'); - }) - - // version up project - if (versionUp) { - displayResult('Saving new version of the project file'); - csi.evalScript('pype.versionUpWorkFile();'); - }; - } else { - // if resulted path file not existing - displayResult('Publish has not been finished correctly. Hit Publish again to publish from already rendered data, or Reset to render all again.'); - }; - - }); - }; - // $('input[name=send-path]').value = ''; - // $('input[name=get-path]').value = ''; -} - -function context() { - var $ = querySelector('#context'); - var project = $('input[name=project]').value; - var asset = $('input[name=asset]').value; - var task = $('input[name=task]').value; - var app = $('input[name=app]').value; - api.context(project, asset, task, app).then(displayResult); -} - -function tc(timecode) { - var seconds = timecodes.toSeconds(timecode); - var timec = timecodes.fromSeconds(seconds); - displayResult(seconds); - displayResult(timec); -} - -function rename() { - var $ = querySelector('#rename'); - var data = {}; - data.ep = $('input[name=episode]').value; - data.epSuffix = $('input[name=ep_suffix]').value; - - if (!data.ep) { - csi.evalScript('pype.alert_message("' + 'Need to fill episode code' + '")'); - return; - }; - - if (!data.epSuffix) { - csi.evalScript('pype.alert_message("' + 'Need to fill episode longer suffix' + '")'); - return; - }; - - csi.evalScript('br.renameTargetedTextLayer( ' + JSON.stringify(data) + ' );', function (result) { - displayResult(result); - }); -} - -// bind buttons -$('#btn-getRernderAnimation').click(function () { - loadAnimationRendersToTimeline(); -}); - -$('#btn-rename').click(function () { - rename(); -}); - -$('#btn-set-context').click(function () { - context(); -}); - -$('#btn-register').click(function () { - register(); -}); - -$('#btn-deregister').click(function () { - deregister(); -}); - -$('#btn-publish').click(function () { - publish(); -}); - -$('#btn-send-reset').click(function () { - var $ = querySelector('#publish'); - $('input[name=send-path]').value = ''; -}); -$('#btn-get-reset').click(function () { - var $ = querySelector('#publish'); - $('input[name=get-path]').value = ''; -}); -$('#btn-get-active-sequence').click(function () { - evalScript('pype.getActiveSequence();'); -}); - -$('#btn-get-selected').click(function () { - $('#output').html('getting selected clips info ...'); - evalScript('pype.getSelectedItems();'); -}); - -$('#btn-get-env').click(function () { - displayResult(window.ENV); -}); - -$('#btn-get-projectitems').click(function () { - evalScript('pype.getProjectItems();'); -}); - -$('#btn-metadata').click(function () { - var $ = querySelector('#publish'); - var path = $('input[name=get-path]').value; - var jsonfile = require('jsonfile'); - displayResult(path); - jsonfile.readFile(path, function (err, json) { - csi.evalScript('pype.dumpPublishedInstancesToMetadata(' + JSON.stringify(json) + ');'); - displayResult('Metadata of clips after publishing were updated'); - }) - - -}); -$('#btn-get-frame').click(function () { - evalScript('$._PPP_.exportCurrentFrameAsPNG();'); -}); - -$('#btn-tc').click(function () { - tc('00:23:47:10'); -}); - -$('#btn-generateRequest').click(function () { - evalScript('pype.getPyblishRequest();'); -}); - -$('#btn-newWorkfileVersion').click(function () { - evalScript('pype.versionUpWorkFile();'); -}); diff --git a/pype/premiere/extensions/com.pype.avalon/ppro/js/build.js b/pype/premiere/extensions/com.pype.avalon/ppro/js/build.js deleted file mode 100644 index ea3f18bf9e..0000000000 --- a/pype/premiere/extensions/com.pype.avalon/ppro/js/build.js +++ /dev/null @@ -1,4862 +0,0 @@ -var app = angular.module("Plugin", ["ui-rangeSlider", "ui.bootstrap"]); -app.run(["$rootScope", "MainHelper", function($rootScope, MainHelper) { - MainHelper.init(BM_VIDEO, 15) -}]), app.controller("ModalIntroController", function($scope, $uibModal, CreateOnFileSystemService, DestinationsService) { - $scope.items = [], $scope.obj = { - state: 1 - }, $scope.$root.$on("intro requested", function(event) { - console.log("ModalIntroController event handler"), $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_INTRO_HTML, - backdrop: "static", - controller: ModalIntroInstanceCtrl, - windowClass: "modal-intro" - }).result.then(function() { - console.log("ModalIntroController OK"), CreateOnFileSystemService.createDestinationBaseFolder(), DestinationsService.saveItem() - }, function() { - console.log("ModalIntroController CANCELED") - }) - } -}); - -var ModalIntroInstanceCtrl = function($scope, $uibModalInstance, BrowseDestinationService, AppModel) { - $scope.obj = { - state: 1, - title: "", - message: "", - labelLeft: [!1, "PREVIOUS"], - labelCenter: [!1, ""], - labelRight: [!0, "NEXT"], - stateImage: [!0, ""], - selectedFolder: AppModel.currentBaseFolder - }, $scope.onChange = function() { - switch (1 < $scope.obj.state && ($scope.obj.stateImage = [!0, STATE_IMG + $scope.obj.state + ".png"]), $scope.obj.state) { - case 1: - $scope.obj.stateName = "", $scope.obj.stateImage = [!1, ""], $scope.obj.labelLeft = [!1, "PREVIOUS"], $scope.obj.title = "Welcome!", $scope.obj.message = "Thanks for downloading the Pond5 Adobe Add-On.
    Click through this short tutorial to learn some of the basics."; - break; - case 2: - $scope.obj.labelLeft = [!0, "PREVIOUS"], $scope.obj.stateName = "search", $scope.obj.title = "", $scope.obj.message = "Start by searching our massive library of royalty-free video clips
    and easily add them to your working projects."; - break; - case 3: - $scope.obj.stateName = "filters", $scope.obj.labelLeft = [!0, "PREVIOUS"], $scope.obj.message = "Use the toolbar on the left to filter your search results,
    view your previews, and update your directory folder."; - break; - case 4: - $scope.obj.stateName = "collections", $scope.obj.message = "View and create new collections below.
    We've even added 50 free clips to get you started!"; - break; - case 5: - $scope.obj.stateName = "login", $scope.obj.labelCenter = [!1, "SELECT"], $scope.obj.labelRight = [!0, "NEXT"], $scope.obj.message = "Log in to your Pond5 account here for easy checkout
    once you've found the perfect clips for your project."; - break; - case 6: - $scope.obj.stateName = "", $scope.obj.labelLeft = [!0, "PREVIOUS"], $scope.obj.labelCenter = [!0, "SELECT"], $scope.obj.labelRight = [!0, "FINISH"], $scope.obj.message = "Select your destination folder to get started. Pond5 media will be saved in this folder.", 0 < AppModel.currentBaseFolder.length && ($scope.obj.message = "Select your destination folder to get started.
    The default folder is " + AppModel.currentBaseFolder) - } - }, $scope.buttonLeftClicked = function() { - $scope.obj.state--, $scope.onChange(), getStateObject($scope.obj.stateName) - }, $scope.buttonCenterClicked = function() { - $scope.obj.selectedFolder = BrowseDestinationService.browse(), $scope.obj.message = "Your current destination folder is:
    " + $scope.obj.selectedFolder - }, $scope.buttonRightClicked = function() { - console.log("ModalIntroController buttonRightClicked"), $scope.obj.state < 6 ? ($scope.obj.state++, $scope.onChange(), getStateObject($scope.obj.stateName)) : (console.log("ModalIntroController buttonRightClicked", $scope.obj.selectedFolder), BrowseDestinationService.save($scope.obj.selectedFolder), $uibModalInstance.close()) - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - }, getStateObject = function(stateName) { - console.log("modalIntroController look for: ", stateName), INTRO_DATA.forEach(function(entry) { - var obj = {}; - entry.stateName === stateName ? (console.log("modalIntroController found stateName: ", entry), obj.stateName = entry.stateName, obj.arrowClass = entry.arrowClass, obj.posX = entry.posX, obj.posY = entry.posY, console.log("modalIntroController found obj: ", obj)) : (obj.stateName = stateName, obj.arrowClass = ""), $scope.$root.$emit("intro asset requested", obj) - }) - }, $scope.onChange() -}; -PLUGIN_VERSION = "", HOST_NAME = "PPRO", THIRD_PARTY = "", MEDIA_TYPES = ["Footage", "Music", "SFX"], BUTTON_REPLACE_LABEL = "REPLACE WITH HI-RES CLIPS", BUTTON_REPLACE_TOOLTIP = "Replace lo-res with paid items", MODAL_REPLACE_HEADER = "Replace With Hi-Res Clips", MODAL_REPLACE_CONTENT = "The selected items below will be replaced by full resolution versions after you complete checkout. Items already in your account history will also be downloaded.", MODAL_REPLACE_RES_TITLE = "RESOLUTION", MODAL_INTRO_SEARCH = "Start by searching our massive library of royalty-free video clips
    and easily add them to your working projects.", MODAL_INTRO_COLLECTIONS = "View and create new collections below.
    We've even added 50 free clips to get you started!", MODAL_INTRO_LOGIN = "Log in to your Pond5 account here for easy checkout
    once you've found the perfect clips for your project.", INTRO_DATA = [{ - state: 7, - stateName: "downloads", - arrowClass: ".intro-asset-arrow-left", - posY: ["top", "96px"], - posX: ["left", "60px"] -}, { - state: 3, - stateName: "filters", - arrowClass: ".intro-asset-arrow-left", - posY: ["top", "60px"], - posX: ["left", "55px"] -}, { - state: 9, - stateName: "destination", - arrowClass: ".intro-asset-arrow-left", - posY: ["bottom", "55px"], - posX: ["left", "60px"] -}, { - state: 4, - stateName: "collections", - arrowClass: ".intro-asset-arrow-down", - posY: ["bottom", "140px"], - posX: ["left", "260px"] -}, { - state: 2, - stateName: "search", - arrowClass: ".intro-asset-arrow-up", - posY: ["top", "60px"], - posX: ["left", "165px"] -}, { - state: 5, - stateName: "login", - arrowClass: ".intro-asset-arrow-up", - posY: ["top", "60px"], - posX: ["right", "75px"] -}], app.service("ReplaceService", ["$rootScope", "ReplaceModel", "Service", "ReplaceServiceShared", function($rootScope, ReplaceModel, Service, ReplaceServiceShared) { - var call = { - onClipFSCollected: function() { - call.getSequences() - }, - getSequences: function() { - csInterface.evalScript("getSequences()", function(result) { - var sequences = JSON.parse(result).sequences; - console.log("\nReplaceService sequences NEW", sequences.length, sequences), ReplaceModel.setSequences(sequences) - }) - }, - getMedia: function() { - var obj = ReplaceModel.sequences; - csInterface.evalScript("getSequenceItems(" + JSON.stringify(obj) + ")", function(result) { - var clipsInSequences = JSON.parse(result).data; - ReplaceModel.clipsInSequences = clipsInSequences, console.log("\nReplaceService clipsInSequences", ReplaceModel.clipsInSequences), csInterface.evalScript("getProjectItems()", function(result) { - call.getMissingItemIDs() - }) - }) - }, - getClipsInSelectedSequences: function() { - for (var clipsInSequences = ReplaceModel.clipsInSequences, clipsInSelectedSequences = [], s = 0; s < ReplaceModel.sequences.length; s++) - for (var j = 0; j < clipsInSequences.length; j++) - if (ReplaceModel.sequences[s].sequenceID === clipsInSequences[j].sequenceID && ReplaceModel.sequences[s].checked) - for (var k = 0; k < clipsInSequences[j].clipNames.length; k++) clipsInSelectedSequences.push(clipsInSequences[j].clipNames[k]); - return clipsInSelectedSequences - }, - getMissingItemIDs: function() { - var clipsInSelectedSequences = call.getClipsInSelectedSequences(); - clipsInSelectedSequences = ReplaceServiceShared.removeDuplicates(clipsInSelectedSequences), console.log("\nReplaceService clipsInSelectedSequences after removing duplicates: ", clipsInSelectedSequences); - var previewNamesonFS = ReplaceServiceShared.getPreviewsOnFSNames(); - clipsInSelectedSequences = ReplaceServiceShared.filterNonP5Clips(clipsInSelectedSequences, previewNamesonFS), console.log("\nReplaceService after filterNonP5Clips", clipsInSelectedSequences); - var previewIDs = ReplaceServiceShared.getPreviewsIDs(clipsInSelectedSequences); - console.log("\nReplaceService previewIDs: " + previewIDs), ReplaceServiceShared.setReplaceProp(previewIDs), console.log("\nReplaceService after set replace: " + ReplaceModel.hiresOnFS); - var hiresIDs = ReplaceServiceShared.getHiresIDsonFS(); - console.log("\nReplaceService hiresIDs: " + hiresIDs); - var missingItemIDs = _(previewIDs).difference(hiresIDs), - missingIDsToString = missingItemIDs.join(","); - 0 < missingItemIDs.length ? Service.getMissingItems(missingIDsToString) : 0 < hiresIDs.length ? call.onPurchasedAndDownloaded() : 0 === clipsInSelectedSequences.length && (ReplaceModel.setState(DEFAULT), $rootScope.$emit("modal simple requested", ["", "There are are currently no Pond5 previews in the sequence(s) you've selected."])) - }, - onPurchasedAndDownloaded: function() { - var hasReplaceCandidates = !1; - if (ReplaceModel.hiresOnFS.forEach(function(entry) { - entry.replace && (hasReplaceCandidates = !0) - }), !hasReplaceCandidates) return $rootScope.$emit("modal simple requested", ["", "Replacing previews by hi-res clips has been canceled"]), void ReplaceModel.setState(DEFAULT); - var obj = { - hiresOnFS: ReplaceModel.hiresOnFS - }; - csInterface.evalScript("replaceClips(" + JSON.stringify(obj) + ")", function(result) { - $rootScope.$emit("modal simple requested", ["", "Your previews have been successfully replaced by your purchased clips. Right-click the clips and choose Scale to Frame Size to scale them correctly."]), ReplaceModel.setState(DEFAULT) - }) - } - }; - return call -}]), app.controller("ModalAddDestinationController", function($scope, $uibModal, UserModel, AppModel, CreateOnFileSystemService, DestinationsService) { - $scope.obj = {}, $scope.$root.$on("modal add destination requested", function() { - console.log("ModalAddDestinationController event handler", UserModel.getFirstTimeUser()), $scope.obj.title = "Add a destination folder", $scope.obj.content = "Please select a new folder to store your previews and purchased items.", $scope.obj.okButtonLabel = "APPLY", $scope.obj.selectedFolderPrefix = "Current folder: ", $scope.obj.selectedFolder = AppModel.currentBaseFolder, $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_ADD_DESTINATION_HTML, - controller: ModalAddDestinatonInstanceCtrl, - size: size, - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - console.log("ModalAddDestinationController OK", AppModel.currentBaseFolder), $scope.onClicked() - }, function() { - console.log("ModalAddDestinationController CANCEL", AppModel.currentBaseFolder), $scope.onClicked() - }) - }, $scope.onClicked = function() { - console.log("ModalAddDestinationController onClicked"), UserModel.getFirstTimeUser() && $scope.$root.$emit("modal freebies"), CreateOnFileSystemService.createDestinationBaseFolder(), DestinationsService.saveItem() - } -}); -var ModalAddDestinatonInstanceCtrl = function($scope, $uibModalInstance, obj, BrowseDestinationService) { - $scope.obj = {}, $scope.obj.showTitle = obj.showTitle, $scope.obj.title = obj.title, $scope.obj.content = obj.content, $scope.obj.selectedFolder = obj.selectedFolder, $scope.obj.selectedFolderPrefix = obj.selectedFolderPrefix, $scope.obj.okButtonLabel = obj.okButtonLabel, $scope.browse = function() { - console.log("ModalAddDestinatonInstanceCtrl browse"), $scope.obj.selectedFolder = BrowseDestinationService.browse() - }, $scope.ok = function() { - BrowseDestinationService.save($scope.obj.selectedFolder), $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalSelectSequencesController", function($scope, $uibModal, ReplaceModel, ReplaceService) { - $scope.items = [], $scope.$root.$on("modal select sequences", function(event, data) { - $scope.items = data, $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_SELECT_SEQUENCES_HTML, - controller: ModalSelectSequencesInstanceCtrl, - size: size, - resolve: { - items: function() { - return $scope.items - } - } - }).result.then(function() { - console.log("ModalSelectSequencesController OK: ", $scope.items); - for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected && (ReplaceModel.sequences[i].checked = !0); - ReplaceService.getMedia() - }, function() { - ReplaceModel.setState(DEFAULT) - }) - } -}); -var ModalSelectSequencesInstanceCtrl = function($scope, $uibModalInstance, items) { - $scope.items = items, $scope.obj = { - showWarning: !1 - }, $scope.ok = function() { - for (var checked = !1, i = 0; i < $scope.items.length; i++) $scope.items[i].selected && (checked = !0); - checked ? $uibModalInstance.close() : $scope.obj.showWarning = !0 - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.factory("MainHelper", ["$rootScope", "AppModel", "StartUpService", "SearchModel", function($rootScope, AppModel, StartUpService, SearchModel) { - var result = { - init: function(mediaType, sumOfBitmasks) { - csInterface = new CSInterface, csInterface.addEventListener("LogEvent", function(evt) { - console.log("JSX : " + evt.data) - }); - var rootFolderPath = csInterface.getSystemPath(SystemPath.EXTENSION); - AppModel.rootFolderPath = rootFolderPath, fs = require("fs"), os = require("os"), path = require("path"), url = require("url"), https = require("https"), xml2js = require(rootFolderPath + "/node_modules/xml2js/lib/xml2js.js"), walk = require(rootFolderPath + "/node_modules/walk/lib/walk.js"), junk = require(rootFolderPath + "/node_modules/junk/index.js"), rimraf = require(rootFolderPath + "/node_modules/rimraf/rimraf.js"), opn = require(rootFolderPath + "/node_modules/opn/index.js"), DecompressZip = require(rootFolderPath + "/node_modules/decompress-zip/lib/decompress-zip.js"), $("#logo").click(function() { - location.reload() - }), result.readManifestXML(), SearchModel.sumOfBitmasks = sumOfBitmasks, $rootScope.$emit("media filter change", mediaType), setTimeout(function() { - AppModel.setEnv() - }, 2e3) - }, - readManifestXML: function() { - var file = AppModel.rootFolderPath + "/CSXS/manifest.xml"; - fs.readFile(file, "utf8", function(err, data) { - if (err) throw err; - result.parseXML(data) - }) - }, - parseXML: function(xml) { - var parser = new xml2js.Parser; - parser.addListener("end", function(res) { - PLUGIN_VERSION = res.ExtensionManifest.$.ExtensionBundleVersion, console.log("mainHelper parsed manifest xml, version:", PLUGIN_VERSION), result.loadJSX() - }), parser.parseString(xml) - }, - loadJSX: function(fileName) { - var jsxPath = AppModel.rootFolderPath + "./js/vendor/json2.js"; - console.log("mainHelper loadJSX:", jsxPath), csInterface.evalScript('$.evalFile("' + jsxPath + '")', function(result) {}) - } - }; - return result -}]), app.service("BrowseDestinationService", ["AppModel", function(AppModel) { - this.browse = function() { - var result = window.cep.fs.showOpenDialog(!1, !0, "Select a folder for your previews and hi-res downloads.", ""), - selectedFolder = AppModel.currentBaseFolder; - return console.log("BrowseDestinationService folder chosen, result.err: ", result.err), 0 == result.err ? (console.log("BrowseDestinationService folder chosen: ", result.data[0]), result.data[0] && (selectedFolder = result.data[0])) : selectedFolder = "This folder cannot be selected. Please choose another folder.", console.log("BrowseDestinationService return folder: ", selectedFolder), selectedFolder - }, this.save = function(selectedFolder) { - console.log("BrowseDestinationService save", AppModel.getOS(), "win" === AppModel.getOS()), "win" === AppModel.getOS() ? AppModel.currentBaseFolder = selectedFolder.replace(/\//g, "\\") : AppModel.currentBaseFolder = selectedFolder - } -}]), app.service("CreateFileCompleteService", ["ImportedPreviewsService", "DestinationsService", "UserService", function(ImportedPreviewsService, DestinationsService, UserService) { - return { - onFileReady: function(file) { - -1 != file.indexOf("imported_previews.xml") && ImportedPreviewsService.readXML(), -1 != file.indexOf("destinations.xml") && DestinationsService.readXML(), -1 != file.indexOf("user.xml") && UserService.readXML() - } - } -}]), app.factory("DestinationsService", ["$rootScope", "AppModel", "UserModel", function($rootScope, AppModel, UserModel) { - var result = { - xmlVersion: "", - readXML: function() { - result.file = AppModel.getDestinationsXML(), console.log("DestinationsService file: ", result.file), fs.readFile(result.file, "utf8", function(err, data) { - if (err) throw err; - result.xml = data, console.log("DestinationsService, xml:", result.xml), result.parseXML() - }) - }, - saveItem: function() { - var node = ''; - result.xml = result.xml.insert(result.xml.indexOf("destinations") + 13, node), result.writeToDisk() - }, - deleteItem: function() {}, - parseXML: function() { - var parser = new xml2js.Parser; - parser.addListener("end", function(res) { - var i; - result.parsedXML = res, AppModel.baseFolders = [], UserModel.setFirstTimeUser(!1), res.root.$[HOST_NAME] ? result.xmlVersion = res.root.$[HOST_NAME] : res.root.$.version ? result.xmlVersion = res.root.$.version : res.root.$.PPRO && (result.xmlVersion = res.root.$.PPRO), UserModel.setUID(res.root.$.id), PLUGIN_VERSION != result.xmlVersion && (console.log("DestinationsService other or no version number in xml, first time user: ", result.xmlVersion), UserModel.setFirstTimeUser(!0)); - var destinations = res.root.destinations[0].destination; - if (console.log("DestinationsService destinations: ", destinations), destinations) { - for (i = 0; i < destinations.length; i++) - 1 == AppModel.baseFolders.indexOf(destinations[i].$.destination) && fs.existsSync(destinations[i].$.destination + path.sep + "pond5") && AppModel.baseFolders.push(destinations[i].$.destination); - fs.stat(AppModel.baseFolders[0] + path.sep + "pond5", function(err, stats) { - err ? setTimeout(function() { - $rootScope.$emit("modal add destination requested") - }, 3e3) : AppModel.currentBaseFolder = AppModel.baseFolders[0] - }), console.log("DestinationsService AppModel.baseFolders : ", AppModel.baseFolders), console.log("DestinationsService currentBaseFolder : ", AppModel.currentBaseFolder) - } - if (UserModel.getFirstTimeUser()) { - var newVersion = HOST_NAME + '="' + PLUGIN_VERSION + '"'; - result.parsedXML.root.$[HOST_NAME] ? result.xml = result.xml.replace(HOST_NAME + '="' + result.xmlVersion + '"', newVersion) : result.parsedXML.root.$.version && "PPRO" === HOST_NAME ? result.xml = result.xml.replace('version="' + result.xmlVersion + '"', newVersion) : result.parsedXML.root.$.version && "PPRO" != HOST_NAME ? result.xml = result.xml.replace('version="' + result.xmlVersion + '"', 'version="' + result.xmlVersion + '" ' + newVersion) : result.parsedXML.root.$.PPRO && !result.parsedXML.root.$[HOST_NAME] && (result.xml = result.xml.replace('PPRO="' + result.xmlVersion + '"', 'PPRO="' + result.xmlVersion + '" ' + newVersion)), console.log("DestinationsService result.xml replaced: ", result.xml), console.log("DestinationsService getFirstTimeUser is true, show intro"), setTimeout(function() { - $rootScope.$emit("intro requested") - }, 3e3) - } - }), parser.parseString(result.xml) - }, - writeToDisk: function() { - fs.writeFile(result.file, result.xml, function(err) { - if (err) throw err; - result.readXML() - }) - } - }; - return result -}]), app.service("ImportService", ["$rootScope", function($rootScope) { - this.importClips = function(items) { - var i, importPaths = []; - for (i = 0; i < items.length; i++) console.log("ImportService item.canceled:", items[i].canceled), items[i].canceled || items[i].imported || (items[i].imported = !0, importPaths.push(items[i].downloadDestination + items[i].fileName)); - console.log("ImportService importPath:", importPaths); - var obj = { - paths: importPaths - }; - csInterface.evalScript("importClips(" + JSON.stringify(obj) + ")", function(result) { - console.log("ImportService result: ", result), $rootScope.$emit("on importing bin complete") - }) - } -}]), app.service("OpenURLService", [function() { - this.openURL = function(url) { - csInterface.openURLInDefaultBrowser(url) - } -}]), app.controller("AdvancedSearchController", function($scope, ViewStateModel, SearchModel, ViewStateService) { - $scope.obj = { - show: !1, - fpsItems: [{ - fps: "23.98" - }, { - fps: "24" - }, { - fps: "25" - }, { - fps: "29.97" - }, { - fps: "30" - }, { - fps: "60" - }, { - fps: "60+" - }], - resItems: [{ - res: "4K+", - param: "8K" - }, { - res: "4K", - param: "4K" - }, { - res: "2K", - param: "2K" - }, { - res: "HD (1080)", - param: "HD1080" - }, { - res: "HD (720)", - param: "HD720" - }, { - res: "SD", - param: "SD" - }, { - res: "Web", - param: "WEB" - }], - showCbFilters: !0, - _minPrice: 0, - _maxPrice: 500, - minPrice: function(newValue) { - return arguments.length ? $scope.obj._minPrice = newValue : $scope.obj._minPrice - }, - maxPrice: function(newValue) { - return 500 == $scope.obj._maxPrice ? $scope.obj.maxPriceValue = "$500+" : $scope.obj.maxPriceValue = "$" + $scope.obj._maxPrice, arguments.length ? $scope.obj._maxPrice = newValue : $scope.obj._maxPrice - }, - _minTime: 0, - _maxTime: 120, - minTime: function(newValue) { - return arguments.length ? $scope.obj._minTime = newValue : $scope.obj._minTime - }, - maxTime: function(newValue) { - return 120 == $scope.obj._maxTime ? $scope.obj.showTimePlusSign = !0 : $scope.obj.showTimePlusSign = !1, arguments.length ? $scope.obj._maxTime = newValue : $scope.obj._maxTime - } - }, $scope.oneAtATime = !0, $scope.reset = function() { - for ($scope.obj._minPrice = 0, $scope.obj._maxPrice = 500, $scope.obj._minTime = 0, $scope.obj._maxTime = 120, SearchModel.fps = "", SearchModel.fpsgt = "", SearchModel.res = "", SearchModel.pricegt = "", SearchModel.pricelt = "", SearchModel.durationgt = "", SearchModel.durationlt = "", i = 0; i < $scope.obj.fpsItems.length; i++) $scope.obj.fpsItems[i].checked = !1; - for (i = 0; i < $scope.obj.resItems.length; i++) $scope.obj.resItems[i].checked = !1 - }, $scope.reset(), $scope.$root.$on("filters button clicked", function(event, state) { - $scope.obj.show = state - }), $scope.$root.$on("media filter change", function(event, data) { - data == BM_VIDEO || data == BM_PUBLIC_DOMAIN ? $scope.obj.showCbFilters = !0 : ($scope.obj.showCbFilters = !1, $scope.reset()), data == BM_AFTER_EFFECTS ? $scope.obj.showDuration = !1 : $scope.obj.showDuration = !0 - }), $scope.change = function() { - var fpsgt, fps = " fps", - res = " resolutions"; - for (i = 0; i < $scope.obj.fpsItems.length - 1; i++) $scope.obj.fpsItems[i].checked && (fps += ":" + $scope.obj.fpsItems[i].fps); - for (fpsgt = $scope.obj.fpsItems[6].checked ? " fpsgt:60" : "", i = 0; i < $scope.obj.resItems.length; i++) $scope.obj.resItems[i].checked && (res += ":" + $scope.obj.resItems[i].param); - fps.length <= 5 ? fps = "" : fpsgt = "", res.length <= 13 && (res = ""), SearchModel.fps = fps, SearchModel.fpsgt = fpsgt, SearchModel.res = res, SearchModel.resultType = "replace", SearchModel.page = 0, ViewStateService.viewRequested("search") - }, $scope.onHideFiltersClicked = function() { - $scope.obj.show = !1, $scope.$root.$emit("filters button clicked", !1) - }, $scope.onResetFiltersClicked = function() { - $scope.reset(), $scope.change() - }, $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, function() { - "cart" !== ViewStateModel.getState() && "downloads" !== ViewStateModel.getState() || ($scope.obj.show = !1) - }, !0), window.addEventListener("rangeSliderOff", function(e) { - "" == $scope.obj._minPrice ? SearchModel.pricegt = "" : SearchModel.pricegt = " pricegt:" + $scope.obj._minPrice, "500" == $scope.obj._maxPrice ? SearchModel.pricelt = "" : SearchModel.pricelt = " pricelt:" + $scope.obj._maxPrice, "" == $scope.obj._minTime ? SearchModel.durationgt = "" : SearchModel.durationgt = " durationgt:" + $scope.obj._minTime, "120" == $scope.obj._maxTime ? SearchModel.durationlt = "" : SearchModel.durationlt = " durationlt:" + $scope.obj._maxTime, $scope.change() - }, !1) -}), app.controller("AlertController", function($scope) { - $scope.alerts = [], $scope.addAlert = function() { - console.log("AlertController add"), $scope.alerts.push({ - msg: "Another alert!" - }) - }, $scope.closeAlert = function(index) { - $scope.alerts.splice(index, 1) - } -}), app.controller("BinsController", function($scope, BinsModel, Service, LoginModel, ViewStateModel, ViewStateService) { - $scope.obj = {}, $scope.obj.showImportAll = !1, $scope.obj.showSelect = !1, $scope.obj.direction = "dropup", $scope.loginModel = function() { - return LoginModel.loggedIn - }, $scope.viewStateModel = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.loginModel, function() { - LoginModel.loggedIn ? $scope.obj.showSelect = !0 : $scope.obj.showSelect = !1 - }), $scope.$watch($scope.viewStateModel, function() { - "bins" != ViewStateModel.getState() && ($scope.obj.selectedNameFormatted = "Collection") - }), $scope.$root.$on("onBins", function(event) { - $scope.bins = BinsModel.bins - }), $scope.onClick = function() { - console.log("BinsController onClick"), $scope.$root.$emit("select clicked") - }, $scope.onChange = function(bin) { - console.log("onChange, bin: ", bin), 14 < bin.name.length ? $scope.obj.selectedNameFormatted = bin.name.substr(0, 14) + "..." : $scope.obj.selectedNameFormatted = bin.name, $scope.obj.open = !1, $scope.selected = bin, $scope.selected && (BinsModel.selectedBin = bin, $scope.$root.$emit("bin selected", bin.name), ViewStateService.viewRequested("bins")) - }, $scope.onDelete = function(bin) { - console.log("onDelete, bin: ", bin) - }, $scope.toggled = function(open) { - $scope.obj.direction = open ? "down" : "dropup" - }, $scope.onAddClicked = function() { - console.log("onAddClicked"), $scope.$root.$emit("modal add collection requested") - }, $scope.onRemoveClicked = function() { - console.log("onRemoveClicked"), $scope.$root.$emit("modal remove collection requested") - } -}), app.controller("CartController", function($scope, Service, ViewStateService, CartModel, LoginModel, AnalyticsService) { - $scope.obj = { - numberOfItem: 0, - clearCartIcon: CLEAR_CART_TRASH_IMG, - imageUrl: CART_BUTTON_IMG, - cartButtonStyle: "button-cart-logged-out" - }, $scope.cartModel = function() { - return CartModel.cartVO - }, $scope.$watch($scope.cartModel, function() { - CartModel.cartVO.items && ($scope.obj.numberOfItems = CartModel.cartVO.items.length) - }), $scope.loginModel = function() { - return LoginModel - }, $scope.$watch($scope.loginModel, function() { - LoginModel.getLoggedIn() ? $scope.obj.cartButtonStyle = "button-cart-logged-in" : ($scope.obj.cartButtonStyle = "button-cart-logged-out", $scope.obj.numberOfItems = "") - }, !0), $scope.onCartButtonClicked = function() { - ViewStateService.viewRequested("cart"); - var ga = { - ec: "cart" - }; - AnalyticsService.sendData(ga) - } -}), app.controller("CheckOutController", function($scope, Service, ViewStateModel, CheckOutService, CartModel) { - $scope.obj = { - show: !1, - disabled: !0, - info: "", - showInfo: !1, - subTotalText: "", - showVAT: !1, - lineStyle: "", - totalStyle: "", - remainingStyle: "", - cartInfoStyle: "" - }, $scope.CartModel = function() { - return CartModel.cartVO - }, $scope.$watch($scope.CartModel, function() { - CartModel.cartVO.items && 0 < CartModel.cartVO.items.length ? $scope.obj.disabled = !1 : $scope.obj.disabled = !0 - }, !0), $scope.$root.$on("checkout complete", function() { - $scope.obj.disabled = !1 - }), $scope.$root.$on("billing info canceled", function() { - $scope.obj.disabled = !1 - }), $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, function() { - "cart" === ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 - }, !0), $scope.onClick = function() { - $scope.obj.disabled = !0, $scope.$root.$emit("on modal choose billing info requested"), $scope.onOut() - }, $scope.onOver = function() { - $scope.obj.showInfo = !0, $scope.showData() - }, $scope.onOut = function() { - $scope.obj.showInfo = !1 - }, $scope.showData = function() { - var data = CartModel.getCartTotal(); - data && ($scope.obj.subTotalText = data.subtotals.beforeDiscounts, data.vatData.display ? $scope.obj.showVAT = !0 : $scope.obj.showVAT = !1, $scope.obj.showVAT ? ($scope.obj.cartInfoStyle = "cart-info-vat", $scope.obj.lineStyle = "cart-info-line-vat", $scope.obj.totalStyle = "cart-info-total-vat", $scope.obj.remainingStyle = "cart-info-remaining-vat", $scope.obj.vatPerc = data.vatData.percentage, $scope.obj.vat = data.vatData.amount) : ($scope.obj.cartInfoStyle = "cart-info-no-vat", $scope.obj.lineStyle = "cart-info-line-no-vat", $scope.obj.totalStyle = "cart-info-total-no-vat", $scope.obj.remainingStyle = "cart-info-remaining-no-vat"), $scope.obj.credits = data.creditsData.usedSum, $scope.obj.total = data.subtotals.final, $scope.obj.remaining = data.creditsData.remainingSum) - }, $scope.$root.$on("alreadyBought", function(event, data) { - CheckOutService.onCheckOutRequested(data) - }), $scope.$root.$on("ownClips", function(event, data) { - CheckOutService.onCheckOutRequested(data) - }) -}), app.controller("CollectionsController", function($scope, BinsModel, Service, LoginModel, ViewStateService) { - $scope.obj = {}, $scope.obj.showImportAll = !1, $scope.obj.showFooter = !1, $scope.obj.showList = !1, $scope.obj.showBin, $scope.obj.addToBin, $scope.obj.addToBinName = "Collections", $scope.obj.collectionsList = COLLECTIONS_LIST_HTML, $scope.loginModel = function() { - return LoginModel.loggedIn - }, $scope.$watch($scope.loginModel, function() { - LoginModel.loggedIn ? $scope.obj.showFooter = !0 : $scope.obj.showFooter = !1 - }), $scope.$root.$on("onBins", function(event) { - $scope.bins = BinsModel.bins, 0 == BinsModel.bins.length && ($scope.obj.addToBinName = "Collections") - }), $scope.$root.$on("active bin changed", function(event) { - $scope.obj.addToBin = BinsModel.addToBin, BinsModel.addToBin && ($scope.obj.addToBinName = getAbbrName(BinsModel.addToBin.name, 10)) - }), $scope.toggleList = function() { - $scope.obj.showList = !$scope.obj.showList - }, $scope.openList = function() { - $scope.obj.showList = !0 - }, $scope.closeList = function() { - $scope.obj.showList = !1 - }, $scope.deleteIconClicked = function(bin) { - $scope.$root.$emit("collection delete requested", [bin]) - }, $scope.showCollectionIconClicked = function(bin) { - BinsModel.showBin = bin, $scope.$root.$emit("bin selected", bin.name), ViewStateService.viewRequested("bins"), $scope.closeList() - }, $scope.collectionNameClicked = function(bin) { - BinsModel.addToBin = bin, $scope.obj.addToBinName = getAbbrName(bin.name, 10), $scope.closeList(), Service.setActiveBin(BinsModel.addToBin.id) - }, $scope.freeItemsClicked = function() { - ViewStateService.viewRequested("freebies"), $scope.closeList() - }, $scope.onClick = function() { - $scope.$root.$emit("select clicked") - }, $scope.onAddClicked = function() { - $scope.$root.$emit("modal add collection requested") - } -}), app.controller("DownloadAllController", function($scope, ViewStateModel, DownloadBatchService, PurchasesModel, AnalyticsService) { - function onStateChange() { - "downloads" === ViewStateModel.getState() && PurchasesModel.purchasesVO && PurchasesModel.purchasesVO.items ? $scope.obj.show = !0 : $scope.obj.show = !1 - } - $scope.obj = { - show: !1, - isDownloading: !1 - }, $scope.$root.$on("on downloading all purchases complete", function(event) { - $scope.$apply(function() { - $scope.obj.isDownloading = !1 - }) - }), $scope.$root.$on("cancel all requested", function(event) { - console.log("DownloadAllController cancel all requested"), $scope.obj.isDownloading = !1 - }), $scope.$root.$on("on purchases vo", function() { - onStateChange() - }), $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, onStateChange, !0), $scope.onDownloadAllClicked = function() { - console.log("DownloadAllController onDownloadAllClicked"), $scope.obj.isDownloading = !0, DownloadBatchService.onBatchRequested(); - var ga = { - ec: "download%20all" - }; - console.log("DownloadAllController ga", ga), AnalyticsService.sendData(ga) - } -}), app.controller("DownloadProgressController", function($scope, $timeout, ProgressService, DownloadRequestService, DownloadCancelService, ViewStateModel, DownloadModel) { - $scope.obj = { - items: [], - isOpen: !1, - progressCloseIcon: PROGRESS_CLOSE_IMG - }, $scope.viewStateModel = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewStateModel, function() { - $scope.obj.view = ViewStateModel.getState() - }), $scope.$root.$on("select clicked", function(event) { - $scope.obj.isOpen = !1 - }), $scope.$root.$on("import all clicked", function(event) { - $scope.obj.isOpen = !0 - }), $scope.$root.$on("open progress", function(event) { - $scope.obj.isOpen || ($scope.obj.isOpen = !0) - }), $scope.$root.$on("clear progress", function(event) { - $scope.obj.items = DownloadModel.itemsDownloadList - }), $scope.$root.$on("added to progress", function(event, data) { - $scope.obj.items = DownloadModel.itemsDownloadList - }), $scope.onProgressIconClicked = function() { - $scope.$root.$emit("progress button clicked") - }, $scope.$root.$on("progress button clicked", function(event) { - $scope.obj.isOpen = !$scope.obj.isOpen - }), $scope.clearListClicked = function() { - $scope.$root.$emit("progress button clicked"), ProgressService.clearCompleteItems(), 0 < $scope.obj.items.length ? $scope.obj.isOpen = !0 : $scope.obj.isOpen = !1 - }, $scope.showClear = function() { - var show = !1; - return $scope.obj.items.forEach(function(item) { - item.completed && (show = !0) - }), !ProgressService.getDownloadingStatus() && 0 < DownloadModel.itemsDownloadList.length && (show = !0), show - }, $scope.isDownloading = function() { - var isDownloading = !1; - return $scope.obj.items.forEach(function(item) { - item.downloading && (isDownloading = !0) - }), ProgressService.getDownloadingStatus() && (show = !0), isDownloading - }, $scope.showMenu = function() { - return 0 < $scope.obj.items.length - }, $scope.cancelAllClicked = function() { - DownloadCancelService.onCancelAll(), $scope.$root.$emit("cancel all requested") - }, $scope.closeClicked = function() { - $scope.$root.$emit("progress button clicked"), console.log("DownloadProgressController closeClicked", $scope.obj.isOpen), $scope.obj.isOpen = !1, console.log("DownloadProgressController closeClicked", $scope.obj.isOpen) - }, $scope.cancelSingleClicked = function(item) { - DownloadCancelService.onCancelSingle(item) - }, $scope.hideTooltip = function() { - $timeout(function() { - $("#clearListButton").trigger("hide") - }, 0) - } -}), app.controller("FilterController", function($scope, Service, SearchModel, ViewStateModel, AnalyticsService) { - $scope.obj = { - filters: ["Best Match", "Popular", "Newest", "Price", "Duration"] - }, $scope.caret = { - direction: "down" - }, $scope.obj.selected = $scope.obj.filters[0], $scope.onChange = function(val) { - var sortID; - switch (console.log("FilterController changed: ", $scope.obj.selected), $scope.obj.selected = val || $scope.obj.selected, $scope.obj.open = !1, $scope.obj.selected) { - case "Best Match": - sortID = 1; - break; - case "ARTIST": - sortID = 2; - break; - case "Newest": - sortID = 6; - break; - case "Duration": - sortID = 5; - break; - case "Popular": - sortID = 8; - break; - case "PAGE VIEWS": - sortID = 10; - break; - case "Price": - sortID = 4 - } - console.log("FilterController sortID: ", sortID), SearchModel.filter = sortID, SearchModel.resultType = "replace", SearchModel.page = "0", Service.search(), window.scrollTo(0, 0); - var ga = {}; - ga.ec = "search%20filter%20" + $scope.obj.selected.replace(/ /g, "%20"), ga.label = SearchModel.query, AnalyticsService.sendData(ga) - }, $scope.setCurrent = function(val) { - $scope.obj.selected = val - }, $scope.toggled = function(open) { - $scope.obj.direction = open ? "dropup" : "down" - } -}), app.controller("FooterLinksController", function($scope, ViewStateModel, CartModel) { - $scope.obj = { - show: !1 - }, $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, function() { - "cart" === ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 - }, !0), $scope.onPromoCodeClicked = function() { - $scope.$root.$emit("modal promo requested") - } -}); -var FreebiesController = function($scope, ViewStateService, FreebiesModel, ViewStateModel, LoginModel, AnalyticsService) { - function onViewStateChange() { - console.log("FreebiesController onViewStateChange:", ViewStateModel.getState()), "freebies" === ViewStateModel.getState() && LoginModel.getLoggedIn() ? $scope.obj.show = !0 : $scope.obj.show = !1 - } - $scope.obj = { - show: !1 - }, $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.loggedIn = function() { - return LoginModel.getLoggedIn() - }, $scope.$watch($scope.viewState, onViewStateChange, !0), $scope.$watch($scope.loggedIn, onViewStateChange), $scope.onFreebiesButtonClicked = function() { - ViewStateService.viewRequested("freebies"), console.log("FreebiesController onFreebiesButtonClicked"); - var ga = { - ec: "freebies" - }; - console.log("FreebiesController ga", ga), AnalyticsService.sendData(ga) - }, $scope.onAddAllFreebiesToCartClicked = function() { - var ids = []; - FreebiesModel.freebiesVO.items.forEach(function(item) { - ids.push(item.id) - }); - var apiObj = { - fn: "modifyCart", - args: [convertArrayToCommaSeperatedString(ids), ""] - }; - $scope.$root.$emit("api call", apiObj), $scope.$root.$emit("modal add to cart") - } -}; -FreebiesController.$inject = ["$scope", "ViewStateService", "FreebiesModel", "ViewStateModel", "LoginModel", "AnalyticsService"], app.controller("ImportCollectionsController", function($scope, DownloadModel, ViewStateModel, BinsModel) { - $scope.obj = { - show: !1, - isImporting: !1 - }, $scope.$root.$on("on importing bin complete", function(event) { - console.log("ImportCollectionsController on importing bin complete"), $scope.$apply(function() { - $scope.obj.isImporting = !1 - }) - }), $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.binsModel = function() { - return BinsModel.binVO - }, $scope.$watch($scope.viewState, function() { - "bins" === ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 - }, !0), $scope.$watch($scope.binsModel, function() { - "bins" === ViewStateModel.getState() && ($scope.obj.show = !0, 0 < BinsModel.binVO.items.length ? $scope.obj.isImporting = !1 : $scope.obj.isImporting = !0) - }, !0), $scope.onImportAllClicked = function() { - $scope.obj.isImporting = !0, $scope.$root.$emit("download requested", BinsModel.binVO.items), $scope.$root.$emit("import all clicked") - } -}), app.controller("IntroAssetsController", function($scope) { - $scope.obj = { - state: 0, - stateName: "" - }, $scope.$root.$on("intro asset requested", function(event, stateObj) { - $scope.obj.stateName = stateObj.stateName, console.log("IntroAssetsController stateName", $scope.obj.stateName); - var fromX, toX, fromY, toY, currArrow = stateObj.arrowClass; - switch (currArrow) { - case ".intro-asset-arrow-up": - fromY = 20, toY = 0; - break; - case ".intro-asset-arrow-left": - fromX = 20, toX = 0; - break; - case ".intro-asset-arrow-down": - fromY = 0, toY = 20 - } - "" != currArrow && ($(currArrow).css("top", "").css("left", "").css("bottom", ""), $(currArrow).css(stateObj.posX[0], stateObj.posX[1]), $(currArrow).css(stateObj.posY[0], stateObj.posY[1]), $(".intro-asset-arrow").velocity("stop"), $scope.loop(currArrow, fromX, toX, fromY, toY)) - }), $scope.loop = function(target, fromX, toX, fromY, toY) { - $(target).velocity({ - translateX: [fromX, toX], - translateY: [fromY, toY] - }, { - duration: 1e3, - loop: !0 - }) - } -}), app.controller("ListItemController", function($scope, VersionsModel, ViewStateModel) { - $scope.obj = {}, $scope.deleteIconClicked = function() { - var apiObj = { - fn: "modifyCart", - args: ["", $scope.item.id] - }; - $scope.$root.$emit("api call", apiObj) - }, $scope.versionButtonClicked = function() { - VersionsModel.setVersions($scope.item.versions) - }, $scope.imageHovered = function(e) { - var item; - "cart" == ViewStateModel.getState() ? item = $scope.item : "downloads" == ViewStateModel.getState() && (item = $scope.item.versions[0]), $scope.$root.$emit("start preview", item) - }, $scope.imageLeft = function(item) { - $scope.$root.$emit("stop preview", item) - } -}), app.controller("ListCartController", function($scope, CartModel) { - $scope.obj = {}, $scope.cartItems = function() { - return CartModel - }, $scope.$watchCollection($scope.cartItems, function() { - CartModel.cartVO && ($scope.obj.items = CartModel.cartVO.items) - }) -}), app.controller("ListDownloadsController", function($scope, PurchasesModel) { - $scope.obj = {}, $scope.purchasedItems = function() { - return PurchasesModel - }, $scope.$watchCollection($scope.purchasedItems, function() { - PurchasesModel.purchasesVO && (console.log("ListController onPurchasesModelChange: ", PurchasesModel.purchasesVO.items), $scope.obj.items = PurchasesModel.purchasesVO.items) - }) -}), app.controller("LoginController", function($scope, LoginModel, UserModel) { - $scope.obj = { - loggedIn: !1, - logo: LOGO_IMG, - logoStyle: "logo-reg" - }, $scope.loginModel = function() { - return LoginModel - }, $scope.userModel = function() { - return UserModel - }, $scope.$watch($scope.loginModel, function() { - void 0 === LoginModel.getLoggedIn() ? $scope.obj.loggedIn = $scope.obj.loggedIn : $scope.obj.loggedIn = LoginModel.getLoggedIn(); - $scope.obj.loggedIn && ($scope.obj.avatarURL = UserModel.getAvatarURL()); - !1 === LoginModel.getLoggedIn() || void 0 === LoginModel.getLoggedIn() ? $scope.obj.row_top_style = "row-top-loggedout" : $scope.obj.row_top_style = "row-top-loggedin" - }, !0), $scope.$watch($scope.userModel, function() { - $scope.obj.avatarURL = UserModel.getAvatarURL(), 0 < THIRD_PARTY.length && ($scope.obj.logo = BASE_URL + "pond5_shared/images/" + THIRD_PARTY + ".png", $scope.obj.logoStyle = "logo-tp") - }, !0), $scope.loginRequested = function() { - $scope.$root.$emit("modal login requested") - }, $scope.logoutClicked = function() { - $scope.$root.$emit("modal logout requested") - } -}), app.controller("MainViewController", function($scope, ViewStateModel, SearchModel) { - $scope.obj = { - tilesClass: "main-content" - }, $scope.$root.$on("filters button clicked", function(event, state) { - $scope.obj.tilesClass = state ? (ViewStateModel.setState("search"), "main-content-advanced-search") : "main-content" - }), $scope.$root.$on("advanced search close requested", function(event) { - $scope.obj.tilesClass = "main-content" - }), $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, function() { - "search" === ViewStateModel.getState() && "add" === SearchModel.resultType ? console.log("MainViewController, do not scroll to top") : window.scrollTo(0, 0); - "cart" !== ViewStateModel.getState() && "downloads" !== ViewStateModel.getState() || ($scope.obj.tilesClass = "main-content"); - $scope.obj.state = ViewStateModel.getState() - }, !0) -}); -var MenuController = function($scope, ViewStateService, AnalyticsService) { - $scope.states = ["default", "hover", "selected"], $scope.btn0 = { - state: $scope.states[2], - selected: !0 - }, $scope.btn1 = { - state: $scope.states[0], - selected: !1 - }, $scope.btn2 = { - state: $scope.states[0], - selected: !1 - }, $scope.btn3 = { - state: $scope.states[0], - selected: !1 - }, $scope.buttons = [$scope.btn0, $scope.btn1, $scope.btn2, $scope.btn3], $scope.click = function(button) { - console.log("MenuController clicked ", button), $scope.selected = button; - for (var i = 0; i < $scope.buttons.length - 1; i++) button === $scope.buttons[i] ? ($scope.buttons[i].selected = !0, $scope.buttons[i].state = $scope.states[2]) : button != $scope.buttons[3] && ($scope.buttons[i].selected = !1, $scope.buttons[i].state = $scope.states[0]); - var view; - switch (button) { - case $scope.buttons[0]: - view = "search"; - break; - case $scope.buttons[1]: - view = "downloads"; - break; - case $scope.buttons[2]: - view = "previews"; - break; - case $scope.buttons[3]: - view = "settings" - } - console.log("MenuController clicked view ", view), $scope.requestView(view) - }, $scope.requestView = function(view) { - "settings" === view ? $scope.$root.$emit("modal add destination requested") : ViewStateService.viewRequested(view); - var ga = {}; - ga.ec = view, console.log("MenuController ga", ga), AnalyticsService.sendData(ga) - }, $scope.over = function(button) { - console.log("MenuController over ", button), button.selected || (button.state = $scope.states[1]) - }, $scope.out = function(button) { - console.log("MenuController over ", button), button.selected || (button.state = $scope.states[0]) - } -}; -MenuController.$inject = ["$scope", "ViewStateService", "AnalyticsService"], app.controller("MessageController", function($scope, ViewStateModel) { - $scope.obj = { - show: !1 - }, $scope.$root.$on("message view requested", function(event, show, data, list, imgUrl) { - $scope.obj.title = null, $scope.obj.messageList = null, $scope.obj.message = null, $scope.obj.imgUrl = null, $scope.obj.showImg = !1, ($scope.obj.show = show) && ($scope.obj.title = data[0], list ? $scope.obj.messageList = data[1] : $scope.obj.message = data[1], 2 === data.length ? $scope.obj.label = "OK" : $scope.obj.label = data[2], imgUrl && ($scope.obj.imgUrl = imgUrl, $scope.obj.showImg = !0)) - }), $scope.viewStateModel = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewStateModel, function() { - "search" !== ViewStateModel.getState() && ($scope.obj.show = !1) - }) -}), app.controller("ModalAddCollectionConfirmationController", function($scope, $uibModal, BinsModel) { - $scope.items = [], $scope.$root.$on("collection created", function(event, data) { - console.log("ModalAddCollectionConfirmationController event handler", data), $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_ADD_COLLECTION_CONFIRMATION_HTML, - controller: ModalAddCollectionConfirmationInstanceCtrl, - size: size, - resolve: { - items: function() { - return $scope - } - } - }).result.then(function() { - console.log("ModalAddCollectionConfirmationController OK") - }, function() { - console.log("ModalAddCollectionConfirmationController CANCELED") - }) - } -}); -var ModalAddCollectionConfirmationInstanceCtrl = function($scope, $uibModalInstance, items, BinsModel) { - $scope.obj = { - title: "Complete!", - messagePre: "Your collection '", - messagePost: "' was succesfully created", - newBinName: BinsModel.newBinName - }, $scope.ok = function() { - $uibModalInstance.dismiss("cancel") - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalAddCollectionController", function($scope, $uibModal, Service, UserModel, BinsModel) { - $scope.items = [], $scope.$root.$on("modal add collection requested", function(event) { - console.log("ModalAddCollectionController event handler"), $scope.open("sm") - }), $scope.open = function(size) { - var modalInstance = $uibModal.open({ - templateUrl: MODAL_ADD_COLLECTION_HTML, - controller: ModalAddCollectionInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - items: function() { - return $scope - } - } - }); - modalInstance.result.then(function() { - console.log("ModalAddCollectionController OK") - }, function() { - console.log("ModalAddCollectionController CANCELED") - }), modalInstance.result.then(function(result) {}, function(result) {}) - } -}); -var ModalAddCollectionInstanceCtrl = function($scope, $uibModalInstance, items, Service, BinsModel) { - $scope.obj = { - showMessage: !1 - }, $scope.create = function() { - console.log("ModalAddCollectionInstanceCtrl bin name: ", document.getElementById("addCollectionInput").value); - var binName = document.getElementById("addCollectionInput").value; - 1 < binName.length && ($uibModalInstance.close(), BinsModel.newBinName = binName, Service.createBin(binName)) - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalAddToCartController", function($scope, $uibModal, Service, ViewStateService) { - $scope.$root.$on("modal add to cart", function(event) { - console.log("ModalAddToCartController event handler"), $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_ADD_TO_CART_HTML, - controller: ModalAddToCartInstanceCtrl, - size: size - }).result.then(function() { - console.log("ModalAddToCartController proceed"), ViewStateService.viewRequested("cart") - }, function() { - console.log("ModalAddToCartController later") - }) - } -}); -var ModalAddToCartInstanceCtrl = function($scope, $uibModalInstance) { - $scope.onProceed = function() { - console.log("ModalAddToCartInstanceCtrl onProceed"), $uibModalInstance.close() - }, $scope.onCancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalBillingAddressController", function($scope, $uibModal) { - $scope.obj = {}, $scope.$root.$on("modal billing address requested", function(event) { - console.log("ModalBillingAddressController event handler"), $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_BILLING_ADDRESS_HTML, - controller: ModalBillingAddressInstanceCtrl, - size: size, - windowClass: "modal-billing-address", - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - console.log("ModalBillingAddressController OK") - }, function() { - console.log("ModalBillingAddressController CANCELED"), $scope.$root.$emit("billing info canceled") - }) - } -}); -var ModalBillingAddressInstanceCtrl = function($scope, $uibModalInstance, obj, Service) { - $scope.firstName = "", $scope.lastName = "", $scope.street1 = "", $scope.street2 = "", $scope.province = "", $scope.zipCode = "", $scope.city = "", $scope.state = "", $scope.country = "", $scope.error = !1, $scope.countries = COUNTRIES, $scope.states = STATES, $scope.submit = function(myForm) { - if (console.log("ModalBillingAddressInstanceCtrl ok: ", myForm.firstName.$modelValue, myForm.lastName.$modelValue), console.log("ModalBillingAddressInstanceCtrl form valid: ", myForm.$valid), myForm.$valid) { - var stateCode; - stateCode = "" == myForm.state.$modelValue ? "" : myForm.state.$modelValue.code; - var data = { - country: myForm.country.$modelValue.code, - firstName: myForm.firstName.$modelValue, - lastName: myForm.lastName.$modelValue, - organization: myForm.organization.$modelValue, - department: myForm.department.$modelValue, - companyID: myForm.companyID.$modelValue, - vatID: myForm.vatID.$modelValue, - street1: myForm.street1.$modelValue, - street2: myForm.street2.$modelValue, - province: myForm.province.$modelValue, - zipCode: myForm.zipCode.$modelValue, - city: myForm.city.$modelValue, - state: stateCode - }; - console.log("ModalBillingAddressInstanceCtrl DATA", data); - var apiObj = { - fn: "setBillingAddress", - args: [data] - }; - $scope.$root.$emit("api call", apiObj), $uibModalInstance.dismiss() - } else console.log("ModalBillingAddressInstanceCtrl form is not valid"), $scope.error = !0 - }, $scope.close = function() { - $uibModalInstance.dismiss() - }, $scope.back = function() { - $uibModalInstance.dismiss(), $scope.$root.$emit("on modal choose billing info requested") - } -}; -app.controller("ModalBuyCreditsController", function($scope, $uibModal, ViewStateModel) { - $scope.obj = {}, $scope.$root.$on("modal buy credits requested", function() { - console.log("ModalBuyCreditsController event handler"), $scope.obj.title = "", $scope.obj.message = "As a reminder, only credits purchased in $USD can be used in this Add-on."; - $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_BUY_CREDITS_HTML, - controller: ModalBuyCreditsInstanceCtrl, - size: size, - resolve: { - obj: function() { - return $scope.obj - } - }, - windowClass: "modal-small" - }).result.then(function() { - console.log("ModalBuyCreditsController OK"), ViewStateModel.allowPreviews = !0, opn("https://www.pond5.com/credit-packages") - }, function() { - console.log("ModalBuyCreditsController CANCELED") - }) - } -}); -var ModalBuyCreditsInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.ok = function() { - console.log("ModalBuyCreditsInstanceCtrl OK"), $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel"), console.log("ModalBuyCreditsInstanceCtrl cancel") - } -}; -app.controller("ModalChooseBillingInfoController", function($scope, $uibModal, BillingInfoModel, CheckOutService, Service) { - $scope.items = [], $scope.obj = {}, $scope.$root.$on("on modal choose billing info requested", function(event) { - console.log("ModalChooseBillingInfoController event handler: ", BillingInfoModel.getBillingInfo()), $scope.items = BillingInfoModel.getBillingInfo(), $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_CHOOSE_BILLING_INFO_HTML, - controller: ModalChooseBillingInfoInstanceCtrl, - windowClass: "modal-choose-billing", - size: size, - resolve: { - items: function() { - return $scope.items - } - } - }).result.then(function(item) { - console.log("ModalChooseBillingInfoController ok, selected: ", item.addressid), CheckOutService.onCheckOutRequested() - }, function() { - console.log("ModalChooseBillingInfoController dismissed"), $scope.$root.$emit("billing info canceled") - }) - } -}); -var ModalChooseBillingInfoInstanceCtrl = function($scope, $uibModalInstance, items, BillingInfoModel, Service) { - console.log("ModalChooseBillingInfoInstanceCtrl items", items), console.log("ModalChooseBillingInfoInstanceCtrl default", BillingInfoModel.getDefaultInfo()), $scope.items = items, $scope.selected = BillingInfoModel.getDefaultInfo(), $scope.adyenEncryption = "https://plugin.pond5.com/pond5_shared/images/adyen-encryption.png", $scope.onRbClicked = function(item) { - $scope.selected = item, console.log("ModalChooseBillingInfoInstanceCtrl rb > default", item), BillingInfoModel.setDefaultInfo(item), Service.getCartTotal() - }, $scope.onOKClicked = function() { - $uibModalInstance.close($scope.selected) - }, $scope.close = function() { - $uibModalInstance.dismiss() - }, $scope.addNewClicked = function() { - $uibModalInstance.dismiss(), $scope.$root.$emit("modal billing address requested") - }, $scope.readAgreement = function() { - console.log("ModalChooseBillingInfoInstanceCtrl readAgreement"), opn("https://www.pond5.com/legal/license") - }, $scope.helpCenter = function() { - opn("https://help.pond5.com/hc/en-us/") - }, $scope.callUs = function() { - opn("https://help.pond5.com/hc/en-us/requests/new") - } -}; -app.controller("ModalChooseFormatController", function($scope, $uibModal) { - $scope.items = [], $scope.$root.$on("on add to cart clicked", function(event, formats) { - console.log("ModalChooseFormatController handler, formats: ", formats), $scope.items = [], $scope.items = formats, $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_CHOOSE_FORMAT_HTML, - controller: ModalChooseFormatInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - items: function() { - return $scope.items - } - } - }).result.then(function() {}, function() { - console.log("ModalChooseFormatController dismissed") - }) - } -}); -var ModalChooseFormatInstanceCtrl = function($scope, $uibModalInstance, items, Service) { - $scope.items = items, $scope.items[0].selected = !0, $scope.onRbClicked = function(item, index) { - console.log("ModalChooseFormatInstanceCtrl onRbClicked: " + item + "-" + index); - for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected = index === i - }, $scope.onAddToCartClicked = function() { - for (var i = 0; i < $scope.items.length; i++) - if ($scope.items[i].selected) { - var item = $scope.items[i], - apiObj = { - fn: "modifyCart", - args: [item.id + ":" + item.offset] - }; - $scope.$root.$emit("api call", apiObj) - } $uibModalInstance.dismiss() - } -}; -app.controller("ModalChooseVersionController", function($scope, $uibModal, Service, DownloadModel) { - $scope.items = [], $scope.$root.$on("on versions selected", function(event, versions) { - console.log("ModalChooseVersionController event handler: ", $scope.items, versions), $scope.items = [], $scope.items = versions, $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_CHOOSE_VERSION_HTML, - controller: ModalChooseVersionInstanceCtrl, - size: size, - resolve: { - items: function() { - return $scope.items - } - }, - windowClass: "modal-small" - }).result.then(function(selectedIndex) { - var selectedItem = $scope.items[selectedIndex]; - DownloadModel.selectedVersion = selectedIndex, Service.getPurchaseURL(selectedItem.id, selectedItem.transactionID, selectedItem.versionID, selectedItem.version) - }, function() { - console.log("ModalChooseVersionController dismissed") - }) - } -}); -var ModalChooseVersionInstanceCtrl = function($scope, $uibModalInstance, items) { - $scope.items = items, $scope.selected = $scope.items[0], $scope.selectedIndex = 0, $scope.onRbClicked = function(index) { - $scope.selected = $scope.items[index], $scope.selectedIndex = index - }, $scope.ok = function() { - $uibModalInstance.close($scope.selectedIndex) - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalClearCartConfirmationController", function($scope, $uibModal) { - $scope.obj = [], $scope.$root.$on("clear cart requested", function(event, data, size) { - console.log("ModalClearCartConfirmationController event handler", data), $scope.obj.title = "Clear My Cart", $scope.obj.message = "Are you sure you want to clear your cart?", $scope.obj.itemsToDelete = data[0], $scope.obj.label = "CLEAR", $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", size = size || "sm", $scope.open(size) - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_SIMPLE_HTML, - controller: ModalClearCartConfirmationInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - console.log("ModalClearCartConfirmationController OK"); - var apiObj = { - fn: "modifyCart", - args: ["", $scope.obj.itemsToDelete] - }; - $scope.$root.$emit("api call", apiObj) - }, function() { - console.log("ModalClearCartConfirmationController CANCELED") - }) - } -}); -var ModalClearCartConfirmationInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalDeleteCollectionConfirmationController", function($scope, $uibModal, Service, ViewStateModel, BinsModel, ViewStateService) { - $scope.obj = {}, $scope.$root.$on("collection delete requested", function(event, data, size) { - console.log("ModalDeleteCollectionConfirmationController event handler", data, data.length, size), $scope.obj.title = "Delete Collection", $scope.obj.message = "Are you sure you want to delete the collection " + data[0].name + "?", $scope.obj.bin = data[0], $scope.obj.label = "DELETE", $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", size = size || "sm", $scope.open(size) - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_SIMPLE_HTML, - controller: ModalDeleteCollectionConfirmationInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - BinsModel.selectedBin == $scope.obj.bin && ViewStateService.viewRequested("search"), Service.removeBin($scope.obj.bin.id), ViewStateModel.allowPreviews = !0 - }, function() {}) - } -}); -var ModalDeleteCollectionConfirmationInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalFreebiesController", function($scope, $uibModal, ViewStateService) { - $scope.$root.$on("modal freebies", function(event) { - console.log("ModalFreebiesController event handler"), $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_FREEBIES_HTML, - controller: ModalFreebiesInstanceCtrl, - size: size - }).result.then(function() { - console.log("ModalFreebiesController OK"), ViewStateService.viewRequested("freebies") - }, function() { - console.log("ModalFreebiesController dismissed") - }) - } -}); -var ModalFreebiesInstanceCtrl = function($scope, $uibModalInstance) { - $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalLoginController", function($scope, $uibModal) { - $scope.obj = {}, $scope.$root.$on("modal login requested", function(event) { - console.log("ModalLoginController event handler"), $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_LOGIN_HTML, - controller: ModalLoginInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - console.log("ModalLoginController OK") - }, function() { - console.log("ModalLoginController CANCELED") - }) - } -}); -var ModalLoginInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.userName = obj.userName, $scope.obj.password = obj.password, $scope.obj.showTitle = !0, $scope.obj.showClose = !0, $scope.loginRequested = function() { - $uibModalInstance.close(); - var apiObj = { - fn: "login", - args: [$scope.obj.userName, $scope.obj.password] - }; - $scope.$root.$emit("api call", apiObj) - }, $scope.close = function() { - $uibModalInstance.dismiss("cancel") - }, $scope.signUp = function() { - opn("https://www.pond5.com/login") - } -}; -app.controller("ModalLogoutConfirmationController", function($scope, $uibModal, Service, ViewStateModel) { - $scope.obj = {}, $scope.$root.$on("modal logout requested", function(event, data, size) { - console.log("ModalLogoutConfirmationController event handler"), $scope.obj.title = "Log out", $scope.obj.message = "Are you sure you want to log out?", $scope.obj.label = "YES", $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", size = size || "sm", $scope.open(size) - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_SIMPLE_HTML, - controller: ModalLogoutConfirmationInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - Service.logout(), ViewStateModel.allowPreviews = !0 - }, function() {}) - } -}); -var ModalLogoutConfirmationInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.showButtonLeft = !0, $scope.obj.labelLeft = "CANCEL", $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalNotLoggedInController", function($scope, $uibModal) { - $scope.obj = {}, $scope.$root.$on("modal not logged in", function(event, data) { - $scope.obj.title = data[0], $scope.obj.message = "You're not logged in", $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_NOT_LOGGED_IN_HTML, - controller: ModalNotLoggedInInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - console.log("ModalNotLoggedInController OK") - }, function() { - console.log("ModalNotLoggedInController CANCELED") - }) - } -}); -var ModalNotLoggedInInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.title = obj.title, $scope.loginRequested = function() { - $uibModalInstance.dismiss("cancel"), $scope.$root.$emit("modal login requested") - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - }, $scope.signUp = function() { - opn("https://www.pond5.com/login") - } -}; -app.controller("ModalPromoCodeController", function($scope, $uibModal, Service, UserModel) { - $scope.items = [], $scope.obj = { - label: "APPLY", - onlyNumbers: /^\d+$/ - }, $scope.$root.$on("modal promo requested", function(event) { - console.log("ModalPromoCodeController event handler"), $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_PROMO_CODE_HTML, - controller: ModalPromoCodeInstanceCtrl, - size: size, - windowClass: "modal-small", - resolve: { - items: function() { - return $scope - } - } - }).result.then(function() { - console.log("ModalPromoCodeController OK") - }, function() { - console.log("ModalPromoCodeController CANCELED") - }) - } -}); -var ModalPromoCodeInstanceCtrl = function($scope, $uibModalInstance, items, Service, $filter) { - $scope.obj = { - showMessage: !1, - label: "APPLY", - onlyNumbers: /^\d+$/ - }, $scope.$root.$on("promo code added", function(event, data) { - var message; - console.log("ModalPromoCodeController event handler", data), message = data.commands[0].sum ? $filter("currency")(data.commands[0].sum) + " were succesfully added to your account!" : "Invalid code. Please try again or contact Pond5.", $scope.obj.credits = data, $scope.obj.showMessage = !0, $scope.obj.message = message, $scope.obj.label = "OK" - }), $scope.codeApplied = function() { - if (console.log("ModalPromoCodeInstanceCtrl codeApplied: ", document.getElementById("promoInput").value), "OK" == $scope.obj.label) $uibModalInstance.close(); - else { - var code = document.getElementById("promoInput").value; - 1 < code.length && Service.promoRedeem(code) - } - }, $scope.ok = function() { - console.log("ModalPromoCodeInstanceCtrl OK"), $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalRemoveCollectionController", function($scope, $uibModal, Service, BinsModel, ViewStateModel) { - $scope.items = [], $scope.showModal = function() { - return BinsModel.showModal - }, $scope.$root.$on("modal remove collection requested", function(event) { - console.log("ModalRemoveCollectionController remove collection requested event handler", BinsModel.showModal, BinsModel.clipClicked), $scope.items = BinsModel.bins, 0 < $scope.items.length && $scope.open() - }), $scope.$root.$on("collection removed", function(event) { - console.log("ModalAddCollectionController collection removed event handler") - }), $scope.open = function(size) { - var modalInstance = $uibModal.open({ - templateUrl: MODAL_REMOVE_COLLECTION_HTML, - controller: ModalRemoveCollectionInstanceCtrl, - windowClass: "modal-fit", - resolve: { - items: function() { - return $scope.items - } - } - }); - $scope.resetBins = function() { - BinsModel.showModal = !1; - for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected = !1 - }, modalInstance.result.then(function() { - console.log("OK: ", BinsModel.clipClicked, $scope.items); - for (var i = 0; i < $scope.items.length; i++) $scope.items[i].selected && (console.log("ModalRemoveCollectionController selected bin:", $scope.items[i].id), Service.removeBin($scope.items[i].id)); - $scope.resetBins(), ViewStateModel.allowPreviews = !0 - }, function() { - $scope.resetBins() - }) - } -}); -var ModalRemoveCollectionInstanceCtrl = function($scope, $uibModalInstance, items) { - $scope.items = items, $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalReplaceController", function($scope, $uibModal, ReplaceModel, ReplaceServiceShared) { - $scope.items = [], $scope.$root.$on("modal replace", function(event, items) { - console.log("ModalReplaceController event handler: ", items), $scope.items = items, $scope.open("lg") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_REPLACE_HTML, - controller: ModalReplaceInstanceCtrl, - size: size, - resolve: { - items: function() { - return $scope.items - } - }, - windowClass: "modal-replace" - }).result.then(function() { - ReplaceServiceShared.onModalReplaceOK() - }, function() { - ReplaceModel.setState(DEFAULT) - }) - } -}); -var ModalReplaceInstanceCtrl = function($scope, $uibModalInstance, items) { - $scope.obj = { - checkIcon: "https://plugin.pond5.com/pond5_shared/images/check-icon.png", - modalHeader: MODAL_REPLACE_HEADER, - modalContent: MODAL_REPLACE_CONTENT, - resTitle: MODAL_REPLACE_RES_TITLE - }, $scope.items = items; - for (var i = 0; i < $scope.items.length; i++) { - $scope.items[i].selected = !0; - for (var j = 0; j < $scope.items[i].formats.length; j++) console.log("ModalReplaceInstanceCtrl incart: ", $scope.items[i].formats[j].inDownloads), $scope.items[i].formats[j].inDownloads && ($scope.items[i].formats.length = 0), 0 < $scope.items[i].formats.length && $scope.items[i].formats[j].inCart && ($scope.items[i].formats[j].selected = !0, $scope.items[i].oneFormatInCart = !0); - !$scope.items[i].oneFormatInCart && 0 < $scope.items[i].formats.length && ($scope.items[i].formats[0].selected = !0) - } - $scope.selectAllClicked = function() { - var item; - console.log("ModalReplaceInstanceCtrl selectAllClicked: ", $scope.obj.selectAll); - for (var i = 0; i < $scope.items.length; i++) item = $scope.items[i], !$scope.obj.selectAll || item.inCart || item.inDownloads ? item.selected = !0 : item.selected = !1 - }, $scope.onRbClicked = function(item, index) { - console.log("ModalReplaceInstanceCtrl onRbClicked: " + item.name + "-" + item.selected); - for (var i = 0; i < item.formats.length; i++) item.formats[i].selected = index === i - }, $scope.onCbClicked = function(item, index) { - console.log("ModalReplaceInstanceCtrl onCbClicked: " + item.name + "-" + item.selected), item.selected = !item.selected; - for (var i = 0; i < item.formats.length; i++) item.formats[i].selected = index === i; - console.log("ModalReplaceInstanceCtrl onCbClicked after toggle: " + item.name + "-" + item.selected) - }, $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalReplaceWarningController", function($scope, $uibModal, Service, DownloadModel, ViewStateService, ReplaceModel) { - $scope.obj = {}, $scope.obj.requestedState = "", $scope.$root.$on("modal replace warning", function(event, viewState) { - console.log("ModalReplaceWarningController event handler, event: ", event), console.log("ModalReplaceWarningController event handler, viewState: ", viewState), $scope.obj.requestedState = viewState, $scope.obj.message = "Visiting the " + viewState + " view will cancel the process of replacing your lo-res previews with hi-res clips. Are you sure you want to visit the " + viewState + " view?", $scope.open("sm") - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_REPLACE_WARNING_HTML, - controller: ModalReplaceWarningInstanceCtrl, - size: size, - resolve: { - obj: function() { - return $scope.obj - } - }, - windowClass: "modal-small" - }).result.then(function() { - ViewStateService.onViewApproved(!0) - }, function() { - console.log("ModalReplaceWarningController CANCELED"), ViewStateService.onViewApproved(!1) - }) - } -}); -var ModalReplaceWarningInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("ModalSimpleController", function($scope, $uibModal, Service, DownloadModel, ViewStateModel) { - $scope.obj = { - imgUrl: "", - showImg: !1 - }, $scope.$root.$on("modal simple requested", function(event, data, size, list, imgUrl) { - var windowClass; - $scope.obj.title = null, $scope.obj.messageList = null, $scope.obj.message = null, $scope.obj.imgUrl = null, $scope.obj.showImg = !1, list ? $scope.obj.messageList = data[1] : $scope.obj.message = data[1], 2 === data.length ? $scope.obj.label = "OK" : $scope.obj.label = data[2], imgUrl && ($scope.obj.imgUrl = imgUrl, $scope.obj.showImg = !0), "sm" === size ? windowClass = "modal-small" : "lg" === size && (windowClass = "modal-large"), $scope.open(windowClass) - }), $scope.open = function(size) { - $uibModal.open({ - templateUrl: MODAL_SIMPLE_HTML, - controller: ModalSimpleInstanceCtrl, - windowClass: size, - resolve: { - obj: function() { - return $scope.obj - } - } - }).result.then(function() { - ViewStateModel.allowPreviews = !0 - }, function() {}) - } -}); -var ModalSimpleInstanceCtrl = function($scope, $uibModalInstance, obj) { - $scope.obj = {}, $scope.obj.message = obj.message, $scope.obj.messageList = obj.messageList, $scope.obj.title = obj.title, $scope.obj.label = obj.label, $scope.obj.imgUrl = obj.imgUrl, $scope.ok = function() { - $uibModalInstance.close() - }, $scope.cancel = function() { - $uibModalInstance.dismiss("cancel") - } -}; -app.controller("PreviewAudioController", function($scope, ViewStateModel) { - $scope.obj = { - show: !1 - }, $scope.$root.$on("start preview", function(event, item, xpos) { - if (("Music" == item.type || "Sound effect" == item.type) && ViewStateModel.allowPreviews) { - var num = Number(item.dur), - seconds = Math.floor(num / 1e3), - minutes = Math.floor(seconds / 60); - 1 === (seconds = seconds - 60 * minutes).toString().length && (seconds = "0" + seconds); - var format = minutes + ":" + seconds; - $scope.obj.dur = format, item.dur || ($scope.obj.dur = ""), $scope.obj.timer = setTimeout(function() { - document.getElementById("tracktime").style.left = "0px", $scope.playAudio(item.m4aURL, xpos), $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps ? $scope.obj.artist = "" : $scope.obj.artist = item.fps, $scope.obj.iconLargeURL = item.iconLargeURL, item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), $scope.$apply(function() { - $scope.obj.show = !0 - }) - }, 400) - } - }), $scope.$root.$on("stop preview", function(event, data) { - data && (clearTimeout($scope.obj.timer), setTimeout(function() { - $scope.playAudio("") - }, 200), $scope.obj.name = "", $scope.obj.price = "", $scope.obj.type = "", $scope.obj.dur = "", $scope.obj.show = !1) - }), $scope.playAudio = function(url, xpos) { - var audio = document.getElementById("audio"); - document.getElementById("source-audio").setAttribute("src", url), audio.load() - } -}), app.controller("PreviewPhotoController", function($scope, ViewStateModel) { - $scope.obj = { - show: !1, - showInfo: !0 - }, $scope.$root.$on("start preview", function(event, item, xpos) { - "Photo" != item.type && "Illustration" != item.type || ViewStateModel.allowPreviews && ($scope.obj.timer = setTimeout(function() { - $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps ? $scope.obj.artist = "" : $scope.obj.artist = item.fps, $scope.obj.vs = item.vs, $scope.obj.ar = item.ar, $scope.obj.audioCodec = item.audioCodec, $scope.obj.videoCodec = item.videoCodec, item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), item.ox ? $scope.obj.res = item.ox + " x " + item.oy : $scope.obj.res = "", $scope.obj.type = item.type, $scope.obj.iconLargeURL = item.iconLargeURL; - var size = convertAspectRatio(370, 208, item.aq); - actualRatio = item.aq, targetRatio = size.x / size.y, adjustmentRatio = targetRatio / actualRatio; - var photo = document.getElementById("photo"); - photo.width = size.x, photo.height = size.y, document.getElementById("preview-loading").style.visibility = "hidden", photo.style.position = "absolute"; - var x_pos = 185 - photo.width / 2; - photo.style.left = x_pos + "px", $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps ? $scope.obj.artist = "" : $scope.obj.artist = item.fps, $scope.obj.fps = item.fps, $scope.obj.vs = item.vs, $scope.obj.ar = item.ar, $scope.obj.audioCodec = item.audioCodec, $scope.obj.videoCodec = item.videoCodec, item.videoCodec && -1 != item.videoCodec.indexOf("Apple ProRes") && ($scope.obj.videoCodec = "Apple ProRes"), item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), item.ox ? $scope.obj.res = item.ox + " x " + item.oy : $scope.obj.res = "", $scope.$apply(function() { - $scope.obj.show = !0 - }) - }, 400)) - }), $scope.$root.$on("stop preview", function(event, item) { - item && (clearTimeout($scope.obj.timer), $scope.obj.name = "", $scope.obj.price = "", $scope.obj.type = "", $scope.obj.show = !1) - }) -}), app.controller("PreviewVideoController", function($scope, ViewStateModel) { - $scope.obj = { - show: !1, - timer: null, - item: null, - showInfo: !0 - }, $scope.$root.$on("start preview", function(event, item) { - "Video" != item.type && "AE" != item.type || ViewStateModel.allowPreviews && ($scope.obj.timer = setTimeout(function() { - $scope.obj.name = item.abbrName, item.artistName ? $scope.obj.artist = "BY " + item.artistName.toUpperCase() : "n/a" === item.fps && ($scope.obj.artist = ""), $scope.obj.fps = item.fps, $scope.obj.vs = item.vs, $scope.obj.ar = item.ar, $scope.obj.audioCodec = item.audioCodec, $scope.obj.videoCodec = item.videoCodec, item.videoCodec && -1 != item.videoCodec.indexOf("Apple ProRes") && ($scope.obj.videoCodec = "Apple ProRes"), item.priceRange && item.priceRange[0] != item.priceRange[1] ? ($scope.obj.price = "$" + item.priceRange[0] + "-$" + item.priceRange[1], $scope.obj.priceStyle = "preview-price-double") : ($scope.obj.price = "$" + item.price, $scope.obj.priceStyle = "preview-price-single"), item.ox ? $scope.obj.res = item.ox + " x " + item.oy : $scope.obj.res = "", $scope.$apply(function() { - $scope.obj.show = !0 - }), $scope.playVideo(item) - }, 400)) - }), $scope.$root.$on("stop preview", function(event, data) { - clearTimeout($scope.obj.timer), $("#video-frame").children().filter("video").each(function() { - this.pause(), $(this).remove() - }), $("#video-frame").empty(), $scope.obj.name = "", $scope.obj.price = "", $scope.obj.fps = "", $scope.obj.vs = "", $scope.obj.show = !1, document.getElementById("preview-loading").style.visibility = "visible" - }), $scope.playVideo = function(item) { - $("#video-frame").append($("")); - var video = document.getElementsByTagName("video")[0], - source = document.getElementById("source-video"); - video.style.visibility = "hidden"; - var size = convertAspectRatio(370, 208, item.aq); - video.addEventListener("loadedmetadata", function(event) { - video.width = size.x, video.height = size.y, document.getElementById("preview-loading").style.visibility = "hidden", video.style.visibility = "visible" - }), item.h264URL ? (video.pause(), source.setAttribute("src", ""), source.setAttribute("src", item.h264URL), video.load()) : (source.setAttribute("src", ""), video.pause()) - }, $scope.$root.$on("preview info icon over", function() { - $scope.obj.showInfo = !0 - }), $scope.$root.$on("preview info icon out", function() { - $scope.obj.showInfo = !1 - }) -}), app.controller("ReplaceController", function($scope, $timeout, ViewStateModel, ReplaceService, LoginModel, AnalyticsService, ReadClipsOnFSService) { - $scope.obj = { - show: !1, - disabled: !1, - buttonLabel: BUTTON_REPLACE_LABEL, - buttonTooltip: BUTTON_REPLACE_TOOLTIP - }, $scope.$root.$on("replacing complete", function() { - $scope.obj.disabled = !1 - }), $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, function() { - "cart" != ViewStateModel.getState() ? $scope.obj.show = !0 : $scope.obj.show = !1 - }, !0), $scope.onReplaceButtonClicked = function() { - if (LoginModel.getLoggedIn()) { - $scope.hideTooltip(), $scope.obj.disabled = !0, ReadClipsOnFSService.listPurchasesOnFS(function() { - console.log("DragAndDropController fs items listed, call onClipsFSCollected"), ReplaceService.onClipFSCollected() - }); - var ga = { - ec: "replace%20with%20hires" - }; - AnalyticsService.sendData(ga) - } else $scope.$root.$emit("modal not logged in", [ERROR]) - }, $scope.onReplaceButtonOver = function() { - $timeout(function() { - $("#replaceButton").trigger("show") - }, 0) - }, $scope.onReplaceButtonOut = function() { - $scope.hideTooltip() - }, $scope.hideTooltip = function() { - $timeout(function() { - $("#replaceButton").trigger("hide") - }, 0) - } -}), app.controller("SearchController", function($scope, ViewStateService, SearchModel, ViewStateModel, AnalyticsService) { - $scope.obj = { - filters: MEDIA_TYPES, - direction: "down", - showFilters: !1, - view: "search", - styleInput: "search-input-reg" - }, $scope.viewStateModel = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewStateModel, function() { - $scope.obj.view = ViewStateModel.getState(), 0 < THIRD_PARTY.length && ($scope.obj.styleInput = "search-input-tp") - }, !0), resizePanel = function() { - var numOfTotalResults = SearchModel.searchResultItems.length, - numOfResults = SearchModel.numOfResults, - rect = window.innerWidth * window.innerHeight; - 0 < numOfResults && numOfResults != numOfTotalResults && numOfTotalResults < rect / 25e3 && "search" == ViewStateModel.getState() && (SearchModel.isSearching || (console.log("SearchController resize, new search"), SearchModel.isSearching = !0, SearchModel.resultType = "add", SearchModel.page = SearchModel.page + 1, ViewStateService.viewRequested("search"))) - }, $scope.obj.selected = $scope.obj.filters[0], $scope.$root.$on("filters button clicked", function(event, state) { - $scope.obj.showFilters = state - }), $scope.filtersRequested = function() { - $scope.obj.showFilters = !$scope.obj.showFilters, $scope.$root.$emit("filters button clicked", $scope.obj.showFilters) - }, $scope.onChange = function(val) { - var sortID; - switch (console.log("SearchController onChange: ", val), $scope.obj.selected = val, $scope.obj.open = !1, $scope.obj.selected) { - case "Footage": - sortID = BM_VIDEO; - break; - case "After Effects": - sortID = BM_AFTER_EFFECTS; - break; - case "Music": - sortID = BM_MUSIC; - break; - case "SFX": - sortID = BM_SFX; - break; - case "Public Domain": - sortID = BM_PUBLIC_DOMAIN; - break; - case "Photos": - sortID = BM_PHOTO; - break; - case "Illustrations": - sortID = BM_ILLUSTRATIONS - } - SearchModel.sumOfBitmasks = sortID, console.log("SearchController changed, selected, bm: ", SearchModel.sumOfBitmasks), $scope.$root.$emit("media filter change", sortID), $scope.search() - }, $scope.setCurrent = function(val) { - $scope.obj.selected = val - }, $scope.toggled = function(open) { - $scope.obj.direction = open ? "dropup" : "down" - }, $scope.search = function() { - var query = document.getElementById("search").value; - "Search Pond5..." === query && (query = ""); - var ga = { - ec: "search" - }; - ga.ea = $scope.obj.selected.replace(/ /g, "%20"), ga.el = query.replace(/ /g, "%20"), AnalyticsService.sendData(ga), SearchModel.query = query, SearchModel.resultType = "replace", SearchModel.page = 0, SearchModel.sumOfBitmasks === BM_PUBLIC_DOMAIN && (SearchModel.query = SearchModel.query + " editorial:1"), console.log("SearchController search: ", query, SearchModel.sumOfBitmasks, SearchModel.resultType, SearchModel.page), ViewStateService.viewRequested("search") - }, $scope.searchButtonClicked = function() { - $scope.search() - }, $scope.enterThis = function() { - 13 === event.keyCode && $scope.search() - }, $scope.onSearchIconClicked = function() { - ViewStateService.viewRequested("search") - } -}); -var SellController = function($scope, AnalyticsService) { - $scope.sellClicked = function() { - var ga = { - ec: "sell%20media" - }; - console.log("SellController ga", ga), AnalyticsService.sendData(ga), opn("https://www.pond5.com/index.php?page=my_uploads") - } -}; -SellController.$inject = ["$scope", "AnalyticsService"], app.controller("SidebarController", function($scope, ViewStateModel, ViewStateService, AnalyticsService) { - $scope.obj = { - view: "search" - }, $scope.viewStateModel = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewStateModel, function() { - $scope.obj.view = ViewStateModel.getState() - }), $scope.onDownloadsIconClicked = function() { - $scope.$root.$emit("views requested", "downloads"), ViewStateService.viewRequested("downloads"); - var ga = { - ec: "downloads" - }; - AnalyticsService.sendData(ga) - }, $scope.onPreviewsIconClicked = function() { - ViewStateService.viewRequested("previews"); - var ga = { - ec: "imported%20previews" - }; - AnalyticsService.sendData(ga) - }, $scope.onDestinationIconClicked = function() { - $scope.$root.$emit("modal add destination requested"); - var ga = { - ec: "add%20destination" - }; - AnalyticsService.sendData(ga) - } -}), app.controller("SubTopRowController", function($scope, ViewStateModel, BinsModel, SearchModel, CartModel, PurchasesModel, UserModel, AnalyticsService) { - function onViewStateChange() { - var title; - switch (ViewStateModel.getState()) { - case "downloads": - title = "MY DOWNLOADS"; - break; - case "previews": - title = "MY IMPORTED PREVIEWS"; - break; - case "cart": - title = "MY CART"; - break; - case "freebies": - title = "50 FREE MEDIA CLIPS"; - break; - case "bins": - console.log("SubTopRowController selected bin name:", BinsModel.showBin.name), title = "COLLECTION: " + BinsModel.showBin.name; - break; - case "search": - title = 0 < SearchModel.query.length ? SearchModel.query.toUpperCase() : ""; - break; - default: - title = "" - } - $scope.obj.title = title, "search" == ViewStateModel.getState() ? $scope.obj.showDropdown = !0 : $scope.obj.showDropdown = !1, "cart" == ViewStateModel.getState() ? $scope.obj.showCreditsWrapper = !0 : $scope.obj.showCreditsWrapper = !1, $scope.showClearAll() - } - $scope.obj = { - showFilters: !1, - titleClass: "sub-top-row-title-no-filters", - showClearAll: !1, - showDropdown: !0, - showCreditsWrapper: !1, - credits: 0 - }, $scope.$root.$on("on cart total", function(event) { - $scope.obj.credits = CartModel.getCartTotal().creditsData.availableSum - }), $scope.cartModel = function() { - return CartModel.cartVO - }, $scope.$watch($scope.cartModel, function() { - $scope.showClearAll() - }), $scope.$root.$on("bin selected", function(event) { - onViewStateChange() - }), $scope.viewStateModelQuery = function() { - return SearchModel.query - }, $scope.$watch($scope.viewStateModelQuery, onViewStateChange), $scope.viewStateModel = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewStateModel, onViewStateChange), $scope.showClearAll = function() { - "cart" == ViewStateModel.getState() && 0 < CartModel.cartVO.items.length ? $scope.obj.showClearAll = !0 : $scope.obj.showClearAll = !1 - }, $scope.$root.$on("filters button clicked", function(event, state) { - $scope.obj.titleClass = state ? "sub-top-row-title-filters" : "sub-top-row-title-no-filters" - }), $scope.onClearCartClicked = function() { - if (0 != CartModel.cartVO.items.length) { - for (var ids = "", i = 0; i < CartModel.cartVO.items.length; i++) i < CartModel.cartVO.items.length ? ids += CartModel.cartVO.items[i].id + "," : ids += CartModel.cartVO.items[i].id; - $scope.$root.$emit("clear cart requested", [ids]) - } - }, $scope.buyCreditsClicked = function() { - var ga = { - ec: "buy%20credits" - }; - console.log("CreditsController ga", ga), AnalyticsService.sendData(ga), $scope.$root.$emit("modal buy credits requested"), console.log("SubTopRowController button clicked") - } -}), app.controller("TileListItemController", function($scope, Service, BinsModel, ImportedPreviewsService, ViewStateModel, LoginModel, ReplaceModel, DownloadModel) { - $scope.childObj = {}, $scope.childObj.addedToCart = !1, $scope.childObj.addedToBin = !1, $scope.allowDownload = !0, $scope.childObj.cartClicked = !1, $scope.childObj.binClicked = !1, $scope.childObj.showEditorial = !0, $scope.childObj.viewState = "search", $scope.childObj.notification = "", "FCPX" === HOST_NAME ? $scope.childObj.importTooltip = "CLICK TO DOWNLOAD" : $scope.childObj.importTooltip = "CLICK TO IMPORT", $scope.viewState = function() { - return ViewStateModel.getState() - }, $scope.$watch($scope.viewState, function() { - $scope.childObj.viewState = ViewStateModel.getState() - }, !0), $scope.$root.$on("added to cart", function(event) { - $scope.childObj.cartClicked && ($scope.childObj.addedToCart = !0), setTimeout(function() { - $scope.childObj.cartClicked = !1, $scope.childObj.addedToCart = !1 - }, 1e3) - }), $scope.$root.$on("added to bin", function(event) { - $scope.childObj.binClicked && ($scope.childObj.addedToBin = !0), setTimeout(function() { - $scope.childObj.binClicked = !1, $scope.childObj.addedToBin = !1 - }, 1e3) - }), $scope.itemHovered = function(e) { - $scope.childObj.showMenu = !0, $scope.$root.$emit("start preview", $scope.item, e.clientX) - }, $scope.itemLeft = function() { - $scope.childObj.showMenu = !1, $scope.$root.$emit("stop preview", $scope.item) - }, $scope.opaqueClicked = function() { - console.log("TileListItemController opaqueClicked", $scope.allowDownload), $scope.allowDownload && ($scope.allowDownload = !1, $scope.$root.$emit("download requested", [$scope.item]), ImportedPreviewsService.saveItem($scope.item.id), $scope.$root.$emit("stop preview", $scope.item)), setTimeout(function() { - $scope.allowDownload = !0 - }, 2e3) - }, $scope.overInfoIcon = function() { - $scope.$root.$emit("preview info icon over") - }, $scope.outInfoIcon = function() { - $scope.$root.$emit("preview info icon out") - }, $scope.binIconClicked = function() { - console.log("TileListItemController binIconClicked"), LoginModel.loggedIn ? 0 < BinsModel.bins.length ? (console.log("TileListItemController binIconClicked show notification"), Service.modifyBin(BinsModel.addToBin.id, $scope.item.id), $scope.childObj.notification = "Added to the collection!", $scope.childObj.binClicked = !0, setTimeout(function() { - $scope.childObj.binClicked = !1, $scope.childObj.addedToBin = !1 - }, 4e3), $scope.childObj.binClicked = !0) : $scope.$root.$emit("modal simple requested", ["You don't have Collections", "In order to add clips to a Collection you first need to create a Collection"]) : $scope.$root.$emit("modal not logged in", [ERROR]) - }, $scope.cartIconClicked = function() { - $scope.childObj.notification = "Added to the cart successfully!", $scope.childObj.cartClicked = !0, setTimeout(function() { - $scope.childObj.cartClicked = !1, $scope.childObj.addedToCart = !1 - }, 4e3), Service.getFormats($scope.item) - }, $scope.trashIconClicked = function() { - $scope.$root.$emit("stop preview", $scope.item), "bins" === ViewStateModel.getState() ? Service.modifyBin(BinsModel.binVO.id, "", $scope.item.id) : "previews" === ViewStateModel.getState() && ImportedPreviewsService.deleteItem($scope.item.id) - }, $scope.linkClicked = function() { - opn("https://www.pond5.com/item/" + $scope.item.id) - } -}), app.controller("TileListSearchController", function($scope, SearchModel, Service) { - $scope.obj = { - showDeleteIcon: !1 - }, $scope.searchItems = function() { - if (SearchModel.searchResultVO) return SearchModel.searchResultVO.items - }, $scope.$watch($scope.searchItems, function() { - SearchModel.searchResultVO && ($scope.obj.items = SearchModel.searchResultItems) - }) -}), app.controller("TileListPreviewsController", function($scope, PreviewsModel) { - $scope.obj = { - showDeleteIcon: !0 - }, $scope.previewItems = function() { - if (PreviewsModel.previewsVO) return PreviewsModel.previewsVO.items - }, $scope.$watch($scope.previewItems, function() { - if (PreviewsModel.previewsVO) { - console.log("TileListPreviewsController: ", PreviewsModel.previewsVO), PreviewsModel.previewsVO.items.reverse(); - for (var previews = PreviewsModel.previewsVO.items, nonAEpreviews = [], i = 0; i < previews.length; i++) "AE" != previews[i].type && nonAEpreviews.push(previews[i]); - $scope.obj.items = nonAEpreviews - } - }) -}), app.controller("TileListBinsController", function($scope, BinsModel) { - $scope.obj = { - showDeleteIcon: !0 - }, $scope.binItems = function() { - if (BinsModel.binVO) return BinsModel.getBinVO() - }, $scope.$watch($scope.binItems, function() { - BinsModel.binVO && ($scope.obj.items = BinsModel.binVO.items) - }, !0) -}), app.controller("TileListFreebiesController", function($scope, FreebiesModel) { - $scope.obj = { - showDeleteIcon: !1 - }, $scope.freeItems = function() { - if (FreebiesModel.freebiesVO) return FreebiesModel.freebiesVO.items - }, $scope.$watch($scope.freeItems, function() { - FreebiesModel.freebiesVO && ($scope.obj.items = FreebiesModel.freebiesVO.items) - }) -}), app.controller("TransactionController", function($scope, ViewStateModel, ViewStateService, Service, AnalyticsService, CheckOutModel, ReplaceModel) { - $scope.obj = { - url: "", - show: !1 - }, $scope.CheckOutModel = function() { - return CheckOutModel - }, $scope.$watch($scope.CheckOutModel, function() { - if (CheckOutModel.checkOutURL) { - (new Date).getTime(); - $scope.obj.url = CheckOutModel.checkOutURL, $scope.obj.show = !0, CheckOutModel.checkOutURL = "", $("body,html").css("overflow", "hidden") - } - }, !0), window.parent.addEventListener("message", function() { - switch (ViewStateModel.allowPreviews = !0, console.log("TransactionController postMessage: ", event.data), event.data) { - case "PAID": - ReplaceModel.getState() === NOT_PURCHASED ? Service.getPurchases() : ($scope.$root.$emit("modal simple requested", PURCHASE_SUCCESSFULL), ViewStateService.viewRequested("downloads")), $scope.$root.$emit("purchase complete"), Service.getUserInfo(), console.log("TransactionController CC payment success"); - break; - case "CANCELED": - $scope.$root.$emit("modal simple requested", PURCHASE_CANCELED); - break; - default: - $scope.$root.$emit("modal simple requested", [ERROR, "UNKNOWN"]) - } - $scope.obj.show = !1, console.log("TransactionController onDone, show:", $scope.obj.show), $scope.$root.$emit("checkout complete"), $("body,html").css("overflow", "visible") - }, !1) -}), app.directive("enter", function() { - return function(scope, element, attrs) { - element.bind("keydown", function() { - 13 === event.which && scope.$apply(attrs.enter) - }) - } -}), app.directive("enterFooter", function() { - return function(scope, element, attrs) { - element.bind("mouseenter", function() { - element.children()[0].style.color = "#ccc" - }) - } -}), app.directive("leaveFooter", function() { - return function(scope, element, attrs) { - element.bind("mouseleave", function() { - element.children()[0].style.color = "#969493" - }) - } -}), app.directive("repositionImage", function() { - return { - restrict: "A", - link: function(scope, elem, attrs) { - elem.on("load", function() { - 108 < $(this).height() && elem.addClass("high") - }) - } - } -}), app.directive("rotate", function() { - return { - restrict: "A", - link: function(scope, element, attrs) { - scope.$watch(attrs.rotate, function(dir) { - var r = "rotate(" + ("up" === dir ? 180 : 0) + "deg)"; - element.css({ - "-webkit-transform": r - }) - }) - } - } -}), app.directive("whenScrolled", ["$window", "ScrollService", function($window, ScrollService) { - return function(scope, elm, attr) { - elm[0]; - angular.element($window).bind("scroll", function() { - ScrollService.onScroll() - }) - } -}]), app.directive("scrollTop", [function() { - return { - restrict: "A", - link: function(scope, $elm, attr) { - scope.$root.$on("scroll progress to top", function() { - $elm.animate({ - scrollTop: 0 - }, "slow") - }) - } - } -}]), app.directive("dragMe", function() { - return { - restrict: "A", - link: function(scope, elem, attr, ctrl) { - elem.draggable() - } - } -}), app.directive("onHoverInfoCart", function() { - return { - link: function(scope, element, attrs) { - element.bind("mouseenter", function($event) { - initialMouseX = $event.clientX, initialMouseY = $event.clientY, scope.$root.$emit("cart icon over", initialMouseX, initialMouseY) - }), element.bind("mouseleave", function() { - scope.$root.$emit("cart icon out") - }) - } - } -}), app.directive("onHoverPreview", function() { - return { - link: function(scope, element, attrs) { - element.bind("mouseenter", function($event) { - var previewX, previewY, tileX = element[0].getBoundingClientRect().left; - previewX = tileX < 310 ? tileX + 220 : tileX - 400, (previewY = element[0].getBoundingClientRect().top - 200) < 20 && (previewY = 20), 340 < previewY && (previewY = 340); - var cols = document.getElementsByClassName("preview"); - for (i = 0; i < cols.length; i++) cols[i].style.left = previewX.toString() + "px", cols[i].style.top = previewY.toString() + "px" - }) - } - } -}), app.filter("to_trusted", ["$sce", function($sce) { - return function(text) { - return $sce.trustAsHtml(text) - } -}]), app.filter("trusted", ["$sce", function($sce) { - return function(url) { - return $sce.trustAsResourceUrl(url) - } -}]), app.filter("secondsToDateTime", [function() { - return function(seconds) { - return new Date(1970, 0, 1).setSeconds(seconds) - } -}]), app.directive("closeCollectionsList", function($document) { - return { - restrict: "A", - link: function(scope, elem, attr, ctrl) { - elem.bind("click", function(e) { - e.stopPropagation() - }), $document.bind("click", function() { - scope.$apply(attr.closeCollectionsList) - }) - } - } -}), app.directive("fieldValidation", function() { - return { - require: "ngModel", - link: function(scope, element, attr, mCtrl) { - mCtrl.$parsers.push(function(value) { - return /^\w+$/.test(value) && 1 < value.toString().length || 0 == value.toString().length ? (mCtrl.$setValidity("charE", !0), console.log("directive valid true")) : (mCtrl.$setValidity("charE", !1), console.log("directive valid false")), value - }) - } - } -}), app.directive("vatValidation", function() { - return { - require: "ngModel", - link: function(scope, element, attr, mCtrl) { - mCtrl.$parsers.push(function(value) { - return /^\w+$/.test(value) && 2 < value.toString().length || 0 == value.toString().length ? (mCtrl.$setValidity("charE", !0), console.log("directive valid true")) : (mCtrl.$setValidity("charE", !1), console.log("directive valid false")), value - }) - } - } -}), app.directive("restrictInput", [function() { - return { - restrict: "A", - link: function(scope, element, attrs) { - var ele = element[0], - regex = RegExp(attrs.restrictInput), - value = ele.value; - ele.addEventListener("keyup", function(e) { - regex.test(ele.value) ? value = ele.value : ele.value = value - }) - } - } -}]), app.filter("searchFilter", function() { - return function(input, param1) { - if (console.log("------------------------------------------------- begin dump of custom parameters"), console.log("searchFilter input: ", input), input && input.length) { - console.log("searchFilter param1: ", param1); - var filteredItems = []; - for (i = 0; i < input.length; i++) input[i].fps == param1 && filteredItems.push(input[i]); - return filteredItems - } - } -}), PURCHASE_SUCCESSFULL = ["Your purchase has been successfull!", "Your items are now ready to download."], PURCHASE_CANCELED = ["Canceled.", "Purchase was canceled."], ERROR = "Oops, something went wrong...", NO_RESULTS = ["Your search returned no results", "
    • Try adjusting your filters
    • Check your search term for misspelling or try a few synonyms
    "], BM_VIDEO = 15, BM_MUSIC = 16, BM_SFX = 32, BM_PHOTO = 128, BM_ILLUSTRATIONS = 1024, BM_AFTER_EFFECTS = 64, BM_PUBLIC_DOMAIN = 16384, MODE = "live", THIRD_PARTY = "", TARGET_APP = "", GA_TRACKING_CODE = "UA-60083218-9", DEFAULT = "not replacing", MISSING_ITEMS = "missing items", NOT_PURCHASED = "not purchased", NOT_DOWNLOADED = "not downloaded", PURCHASED_AND_DOWNLOADED = "purchased and downloaded"; -var BASE_URL = "https://plugin.pond5.com/", - NO_RESULTS_ICON = BASE_URL + "pond5_shared/images/no_results_icon.png", - DRAGNDROP_IMG = BASE_URL + "pond5_shared/images/intro-icons/dragndrop.png", - STATE_IMG = BASE_URL + "pond5_shared/images/intro-states/step", - STATE_FCP_IMG = BASE_URL + "pond5_shared/images/intro-states-fcp/step", - DOWNLOAD_IMG = BASE_URL + "pond5_shared/images/intro-icons/download.png", - CART_IMG = BASE_URL + "pond5_shared/images/intro-icons/cart.png", - PREVIEWS_IMG = BASE_URL + "pond5_shared/images/intro-icons/previews.png", - DUMMY_IMG = BASE_URL + "pond5_shared/images/intro-icons/dummy.png", - CLEAR_CART_TRASH_IMG = BASE_URL + "pond5_shared/images/clear-cart-trash-icon.png", - CART_BUTTON_IMG = BASE_URL + "pond5_shared/images/cartButtonIcon.png", - PROGRESS_CLOSE_IMG = BASE_URL + "pond5_shared/images/progress-close-icon.png", - LOGO_IMG = BASE_URL + "pond5_shared/images/logo-white.png", - MODAL_SIMPLE_HTML = BASE_URL + "pond5_shared/views/modals/modalSimple.html", - MODAL_ADD_DESTINATION_HTML = BASE_URL + "pond5_shared/views/modals/modalAddDestination.html", - MODAL_ADD_COLLECTION_HTML = BASE_URL + "pond5_shared/views/modals/modalAddCollection.html", - MODAL_ADD_COLLECTION_CONFIRMATION_HTML = BASE_URL + "pond5_shared/views/modals/modalAddCollectionConfirmation.html", - MODAL_SELECT_SEQUENCES_HTML = BASE_URL + "pond5_shared/views/modals/modalSelectSequences.html", - MODAL_INTRO_HTML = BASE_URL + "pond5_shared/views/modals/modalIntro.html", - MODAL_ADD_TO_CART_HTML = BASE_URL + "pond5_shared/views/modals/modalAddToCart.html", - MODAL_BILLING_ADDRESS_HTML = BASE_URL + "pond5_shared/views/modals/modalBillingAddress.html", - MODAL_CHOOSE_BILLING_INFO_HTML = BASE_URL + "pond5_shared/views/modals/modalChooseBillingInfo.html", - MODAL_CHOOSE_FORMAT_HTML = BASE_URL + "pond5_shared/views/modals/modalChooseFormat.html", - MODAL_CHOOSE_VERSION_HTML = BASE_URL + "pond5_shared/views/modals/modalChooseVersion.html", - MODAL_FREEBIES_HTML = BASE_URL + "pond5_shared/views/modals/modalFreebies.html", - MODAL_LOGIN_HTML = BASE_URL + "pond5_shared/views/modals/modalLogin.html", - MODAL_NOT_LOGGED_IN_HTML = BASE_URL + "pond5_shared/views/modals/modalNotLoggedIn.html", - MODAL_PROMO_CODE_HTML = BASE_URL + "pond5_shared/views/modals/modalPromoCode.html", - MODAL_REMOVE_COLLECTION_HTML = BASE_URL + "pond5_shared/views/modals/modalRemoveCollection.html", - MODAL_REPLACE_HTML = BASE_URL + "pond5_shared/views/modals/modalReplace.html", - MODAL_REPLACE_WARNING_HTML = BASE_URL + "pond5_shared/views/modals/modalReplaceWarning.html", - MODAL_BUY_CREDITS_HTML = BASE_URL + "pond5_shared/views/modals/modalBuyCredits.html", - COLLECTIONS_LIST_HTML = BASE_URL + "pond5_shared/views/collectionsList.html"; -$(function() { - Offline.options = { - checkOnLoad: !0, - checks: { - image: { - url: function() { - return "https://plugin.pond5.com/pond5_shared/images/logo-white.png?_=" + Math.floor(1e9 * Math.random()) - } - }, - active: "image" - } - } -}), app.service("AppModel", ["$rootScope", function($rootScope) { - var path = require("path"), - dirHomePond5 = getUserHome() + path.sep + "pond5", - dirImports = dirHomePond5 + path.sep + "imports", - dirPrefs = dirHomePond5 + path.sep + "prefs", - dirDestinations = dirHomePond5 + path.sep + "destinations", - dirDefaultLib = path.sep, - dirUser = dirHomePond5 + path.sep + "user", - result = (dirDefaultLib = dirHomePond5 + path.sep + "defaultLib", { - OS: "", - baseFolders: [], - currentBaseFolder: "", - previewsDir: "", - purchasedDir: "", - defaultLib: "", - defaultLibName: "", - defaultLibPath: "", - targetApp: "", - setEnv: function() { - result.setOS(os.platform()), $rootScope.$emit("environment set") - }, - getOS: function() { - return result.OS - }, - setOS: function(s) { - result.OS = s - }, - getDocumentsPath: function() { - return os.homedir() + path.sep + "Documents" - }, - getDirHomePond5: function() { - return dirHomePond5 - }, - getDirImports: function() { - return dirImports - }, - getDirDestinations: function() { - return dirDestinations - }, - getDirPrefs: function() { - return dirPrefs - }, - getDirUser: function() { - return dirUser - }, - getDestinationsXML: function() { - return result.getDirDestinations() + path.sep + "destinations.xml" - }, - getUserXML: function() { - return result.getDirUser() + path.sep + "user.xml" - }, - getPreferencesXML: function() { - return result.getDirPrefs() + path.sep + "preferences.xml" - }, - getDirDefaultLib: function() { - return dirDefaultLib - }, - getDefaultLib: function() { - return result.defaultLib - }, - setDefaultLib: function(path) { - "/" == path.substr(path.length - 1) && (path = path.slice(0, -1)), result.setDefaultLibName(path), result.setDefaultLibPath(path), result.defaultLib = path - }, - getDefaultLibName: function() { - return result.defaultLibName - }, - setDefaultLibName: function(path) { - var n = path.lastIndexOf("/"); - result.defaultLibName = path.substring(n + 1).replace(".fcpbundle", "") - }, - getDefaultLibPath: function() { - return result.defaultLibPath - }, - setDefaultLibPath: function(path) { - result.defaultLibPath = path.substring(0, path.lastIndexOf("/")) - }, - getDefaultLibXML: function() { - return result.getDirDefaultLib() + path.sep + "defaultLib.xml" - }, - getTargetApp: function() { - return result.targetApp - }, - setTargetApp: function(app) { - result.targetApp = app - } - }); - return result -}]), app.factory("BillingInfoModel", ["$rootScope", function($rootScope) { - var info = { - onBillingInfo: function(data) { - info.setBillingInfo(data.commands[0]), info.getBillingInfo().forEach(function(item) { - item.isdefault && info.setDefaultInfo(item) - }) - }, - setBillingInfo: function(data) { - info.billingInfo = data - }, - getBillingInfo: function() { - return info.billingInfo - }, - setDefaultInfo: function(data) { - info.defaultInfo = data - }, - getDefaultInfo: function() { - return info.defaultInfo - } - }; - return info -}]), app.service("BinsModel", ["$rootScope", function($rootScope) { - var result = { - binsVO: null, - bins: [], - binVO: null, - showBin: null, - addToBin: null, - onBins: function(data) { - result.binsVO = new BinsVO(data.commands[0]), result.bins = result.binsVO.bins, $rootScope.$emit("onBins") - }, - onBin: function(data) { - result.setBinVO(new BinVO(data.commands[0])) - }, - onActiveBin: function(data) { - result.bins.forEach(function(bin) { - bin.id == data.commands[0].binid && (result.addToBin = bin) - }), $rootScope.$emit("active bin changed", result.addToBin) - }, - setBinVO: function(data) { - result.binVO = data - }, - getBinVO: function() { - return result.binVO - } - }; - return result -}]); -var BinsVO = function BinsVO(data) { - var i; - for (this.bins = [], i = 0; i < data.bins.length; i += 1) { - var bin = {}; - bin.name = data.bins[i].name, bin.abbrBinName = getAbbrName(bin.name, 17), bin.id = data.bins[i].id, bin.total = data.bins[i].tot, bin.selected = !1, this.bins[i] = bin - } - this.bins.sort(compare), BinsVO.prototype = { - toString: function() { - console.log("bins: " + this.bins) - } - } - }, - BinVO = function BinVO(data) { - var itemVO, i; - this.items = [], this.id = data.binid, this.name = data.name, this.jpegBase = "http://ec.pond5.com/s3/", console.log("BinVO id: ", data.binid, data.name); - var filterVS = 0; - for (filterVS = "AEFT" == HOST_NAME ? 200 : 102, i = 0; i < data.items.length; i += 1) parseInt(data.items[i].vs) <= filterVS && (itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base, "", this.jpegBase), this.items.push(itemVO)); - BinVO.prototype = { - toString: function() { - console.log("name & id: ", this.id, this.name) - } - } - }; -app.factory("CartModel", ["$rootScope", "ReplaceModel", function($rootScope, ReplaceModel) { - $rootScope.$on("on cart", function(event, data) { - result.onCart(data) - }), $rootScope.$on("on cart total", function(event, data) { - result.onCartTotal(data) - }), $rootScope.$on("formats complete", function(event, item, formats) { - console.log("CartModel onCart ReplaceModel.getState(): ", ReplaceModel.getState()), result.onFormats(item, formats) - }); - var result = { - cartVO: [], - cartTotal: null, - onCart: function(data) { - result.cartVO = new ItemsVO(data.commands[0]) - }, - onCartTotal: function(data) { - result.setCartTotal(data.commands[0]) - }, - onFormats: function(item, formats) { - if (console.log("CartModel onFormats, num of formats for id: ", item, formats.length), 1 < formats.length) { - var uniqueResFormats = _.uniq(formats, function(p) { - return p.ti - }); - $rootScope.$emit("on add to cart clicked", uniqueResFormats) - } else { - var apiObj = { - fn: "modifyCart", - args: [item.id, ""] - }; - $rootScope.$emit("api call", apiObj) - } - }, - setCartTotal: function(data) { - result.cartTotal = data - }, - getCartTotal: function() { - return result.cartTotal - } - }; - return result -}]), app.factory("CheckOutModel", ["$sce", function($sce) { - var result = { - onPurchase: function(data) { - console.log("CheckOutModel onPurchase, url: ", data.commands[0].url); - (new Date).getTime(); - result.checkOutURL = $sce.trustAsResourceUrl(data.commands[0].url), console.log("CheckOutModel onPurchase, url: ", result.checkOutURL) - } - }; - return result -}]), app.factory("DownloadModel", ["$rootScope", "PurchasesModel", "ReplaceModel", function($rootScope, PurchasesModel, ReplaceModel) { - var result = { - binBatch: null, - itemsDownloadList: [], - selectedVersion: 0, - downloadingBatchURLs: !1, - urlCounter: 0, - downloadCounter: -1, - stayAwake: !1, - onGetPurchaseURL: function(data) { - var item = result.getVersionByID(data.commands[0].bid); - item && (item.hiresURL = data.commands[0].url, item.downloadType = "purchase", "AE" == item.vs && (item.type = item.vs), $rootScope.$emit("download requested", [item])) - }, - onGetAllPurchaseURLs: function(data) { - var i, purchase, purchases = []; - for (ReplaceModel.getState() === DEFAULT ? purchases = PurchasesModel.purchasesVO.items : ReplaceModel.getState() === NOT_DOWNLOADED && (purchases = ReplaceModel.missingDownloads), result.urlCounter++, i = 0; i < purchases.length; i += 1) { - purchase = purchases[i]; - var dataItem = data.commands[0]; - for (k = 0; k < purchase.formats.length; k += 1) purchase.formats[k].id == dataItem.bid && (purchase.hiresURL = dataItem.url, purchase.downloadType = "purchase"); - purchase.id == dataItem.bid && (purchase.hiresURL = dataItem.url, purchase.downloadType = "purchase", purchase.versions && 0 < purchase.versions.length && (purchase.vs = purchase.versions[0].vs)) - } - purchases = purchases.filter(function(v, i, a) { - return a.indexOf(v) == i - }), result.urlCounter === purchases.length && ($rootScope.$emit("download requested", purchases), result.urlCounter = 0, result.downloadingBatchURLs = !1) - }, - getVersionByID: function(id) { - var foundItem; - if (PurchasesModel.purchasesVO.items.forEach(function(item) { - item.id === id && (item.parentFormatID && (item.versions[result.selectedVersion].parentFormatID = item.parentFormatID), foundItem = item.versions[result.selectedVersion]) - }), foundItem) return foundItem - } - }; - return result -}]), app.factory("FreebiesModel", [function() { - var result = { - onFreebies: function(data) { - result.freebiesVO = new ItemsVO(data.commands[0]) - } - }; - return result -}]); -var HiresVO = function HiresVO(dest, name) { - this.dest = dest, this.name = name, this.path = dest + name, this.id = name.split(" ")[1], this.replace = !1, this.type = "", this.nameFCP = this.name.replaceAll(" ", "%20"), this.nameFCP = this.nameFCP.replaceAll("-", "%2D"), this.nameFCP = this.nameFCP.replaceAll("&", "and"), this.pathFCP = "file://" + this.path.replaceAll(" ", "%20"), this.pathFCP = this.pathFCP.replaceAll("-", "%2D"), this.pathFCP = this.pathFCP.replaceAll("&", "and"), HiresVO.prototype = { - toString: function() { - return "\nHiresVO path: " + this.path + "\nname: " + this.name + "\nid: " + this.id + "\nreplace: " + this.replace - } - } - }, - ItemsVO = function ItemsVO(data) { - var itemVO, i; - for (this.tot_nbr_rows = data.tot_nbr_rows, this.max_per_page = data.max_per_page, this.nbr_footage = data.nbr_footage, this.nbr_music = data.nbr_music, this.nbr_sfx = data.nbr_sfx, this.nbr_total = data.nbr_total, this.items = [], i = 0; i < data.items.length; i += 1) itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base, ""), this.items[i] = itemVO; - ItemsVO.prototype = { - toString: function() { - console.log("vs: " + this.vs) - } - } - }, - ItemVO = function ItemVO(data, iconBase, flvBase, parentID) { - var getURL; - this.selectedVersion = 0, this.name = data.n, this.abbrName = getAbbrName(this.name, 25), this.abbrTileName = getAbbrName(this.name, 22), this.abbrListName = getAbbrName(this.name, 40), this.artistName = getAbbrName(data.artistname, 40), this.id = data.id, this.title = data.ti, this.vr360 = data.vr360, data.pr < .001 ? this.price = "0" : this.price = data.pr, this.priceRange = data.pricerange, this.vs = getConvertedVideoStandard(data.vs), this.downloadType = "preview", this.downloadURL, this.downloadDestination = "", this.downloading = !1, this.progressPerc = "", this.progressMB = "", this.progressName = "", this.parentFormatID = "", this.canceled = !1, this.completed = !1, this.imported = !1, this.inCart = !1, this.inDownloads = !1, this.selected = !1, this.formats = [], this.versions = [], this.ox = data.ox, this.oy = data.oy, this.ar = getAspectRatio(data.ar), this.ar || (this.ar = "n/a"), this.aq = data.aq, this.dur = data.dur, data.fps ? this.fps = data.fps : this.fps = "n/a", data.ti && (this.title = data.ti), data.tb && (this.subTitle = data.tb), data.i && (this.additionalInfo = data.i), data.id ? this.id = data.id : this.id = parentID, 0 === this.id.length && (this.id = parentID), this.offset = data.so, this.transactionID = data.tr, this.expirationDate = data.exp, this.versionID = data.v, this.videoCodec = data.codg, this.audioCodec = data.coda, this.extension = data.ext, this.version = data.bitoffset, this.type = getMediaType(this.vs), this.baseURL = flvBase || "https://api-cdn.pond5.com/", getURL = function(id, type, baseURL) { - var url; - switch (type) { - case "icon": - url = iconBase + ExtendedID.extend(id) + "_iconv.jpeg"; - break; - case "H264": - url = baseURL + ExtendedID.extend(id) + "_main_xl.mp4"; - break; - case "vr360": - url = baseURL + ExtendedID.extend(id) + "_main360.mp4"; - break; - case "mov": - url = baseURL + ExtendedID.extend(id) + "_prev_264.mov"; - break; - case "flv": - url = baseURL + ExtendedID.extend(id) + "_prev_xl.flv"; - break; - case "mp3": - url = baseURL + ExtendedID.extend(id) + "_prev.mp3"; - break; - case "m4a": - url = baseURL + ExtendedID.extend(id) + "_prev.m4a"; - break; - case "icon large": - url = iconBase + ExtendedID.extend(id) + "_iconl.jpeg" - } - return url - }, this.iconURL = getURL(this.id, "icon", this.baseURL), this.iconLargeURL = getURL(this.id, "icon large", this.baseURL), this.vr360 ? this.h264URL = getURL(this.id, "vr360", this.baseURL) : this.h264URL = getURL(this.id, "H264", this.baseURL), this.mp3URL = getURL(this.id, "mp3", this.baseURL), this.m4aURL = getURL(this.id, "m4a", this.baseURL), ItemVO.prototype = {} - }; -app.factory("LoginModel", [function() { - var data = { - getLoggedIn: function() { - return data.loggedIn - }, - setLoggedIn: function(state) { - data.loggedIn = state - }, - getCX: function() { - return data.cx - }, - setCX: function(cx) { - data.cx = cx - }, - getCM: function() { - return data.cm - }, - setCM: function(cm) { - data.cm = cm - } - }; - return data -}]), app.service("MissingItemsModel", [function() { - return { - missingItemsVO: null - } -}]); -var MissingItemsVO = function MissingItemsVO(data) { - var i; - for (this.items = [], i = 0; i < data.items.length; i += 1) this.itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base), this.items[i] = this.itemVO; - MissingItemsVO.prototype = {} -}; -app.factory("PreviewsModel", [function() { - var result = { - onPreviews: function(data) { - console.log("PreviewsModel onPreviews: ", data), result.previewsVO = new ItemsVO(data.commands[0]) - } - }; - return result -}]); -var PreviewVO = function PreviewVO(dest, path) { - var parts = (this.path = path).split("/"); - this.name = parts[parts.length - 1], this.id = this.name.split(" ")[0], PreviewVO.prototype = { - toString: function() { - return "\nPreviewVO path: " + this.path + "\nname: " + this.name + "\nid: " + this.id - } - } -}; -app.service("PurchasesModel", ["$rootScope", "AnalyticsService", function($rootScope, AnalyticsService) { - $rootScope.$on("on purchases", function(event, data) { - result.onGetPurchases(data) - }), $rootScope.$on("purchase complete", function(event) { - console.log("PurchasesModel purchase complete handler"), result.sendGA = !0 - }); - var result = { - purchasesVO: [], - sendGA: !1, - onGetPurchases: function(data) { - result.purchasesVO = new PurchaseVO(data.commands[0]), $rootScope.$emit("on purchases vo", result.purchasesVO), console.log("PurchasesModel onGetPurchases result.purchasesVO: ", result.purchasesVO), result.sendGA && (AnalyticsService.sendData(result.purchasesVO, "transaction"), result.sendGA = !1) - } - }; - return result -}]); -var PurchaseVO = function PurchaseVO(data) { - var i; - this.items = []; - for ("AEFT" == HOST_NAME ? 200 : 102, i = 0; i < data.items.length; i += 1) { - var j; - for (this.itemVO = new ItemVO(data.items[i], data.icon_base, data.flv_base, data.items[i].bid), this.itemVO.transactionID = data.items[i].versions[0].tr, this.itemVO.name = data.items[i].versions[0].n, this.itemVO.abbrName = getAbbrName(this.itemVO.name, 30), this.itemVO.expirationDate = data.items[i].versions[0].exp, this.itemVO.parentFormatID = data.items[i].versions[0].vm, this.itemVO.type = getMediaType(getConvertedVideoStandard(data.items[i].versions[0].vs)), this.itemVO.aq = data.items[i].versions[0].aq, this.itemVO.versionID = data.items[i].versions[0].v, this.itemVO.version = data.items[i].versions[0].bitoffset, j = 0; j < data.items[i].versions.length; j += 1) this.itemVO.versions[j] = new ItemVO(data.items[i].versions[j], data.icon_base, data.flv_base, data.items[i].bid); - this.items.push(this.itemVO) - } - PurchaseVO.prototype = { - toString: function() { - console.log("name & id: ", this.items) - } - } -}; - -function checkNested(obj) { - for (var args = Array.prototype.slice.call(arguments), i = (obj = args.shift(), 0); i < args.length; i++) { - if (!obj.hasOwnProperty(args[i])) return !1; - obj = obj[args[i]] - } - return !0 -} - -function compare(a, b) { - return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 -} - -function sortArgs() { - return Array.prototype.slice.call(arguments, 0).sort()[0] -} - -function getAspectRatio(as) { - var standard; - switch (as) { - case 1: - standard = "4:3"; - break; - case 2: - standard = "16:9 anamorphic"; - break; - case 3: - standard = "16:9 letterboxed"; - break; - case 4: - standard = "n/a"; - break; - case 5: - standard = "Other"; - break; - case 6: - standard = "16:9 native" - } - return standard -} - -function convertAspectRatio($max_x, $max_y, $aspect_quotient) { - var $out_x, $out_y; - return $aspect_quotient ? ($out_y = $max_y, $max_x < ($out_x = Math.round($max_y * parseFloat($aspect_quotient))) && ($out_x = $max_x, $out_y = Math.round($max_x / parseFloat($aspect_quotient))), new Point($out_x, $out_y)) : ($out_x = $max_x, $out_y = $max_y, new Point(370, 208)) -} -app.factory("ReplaceModel", ["$rootScope", function($rootScope) { - var result = { - clipsInSequences: [], - aeItemsinProjectView: [], - state: DEFAULT, - missingDownloads: [], - hiresOnFS: [], - previewsOnFS: [], - sequences: [], - setState: function(newState) { - result.state = newState, console.log("ReplaceModel STATE:", result.state), result.state === DEFAULT && $rootScope.$root.$emit("replacing complete") - }, - getState: function() { - return result.state - }, - getAEItems: function() { - return result.aeItemsinProjectView - }, - setAEItems: function(items) { - result.aeItemsinProjectView = items - }, - setSequenceNames: function(seqNames) { - result.sequences = []; - for (var i = 0; i < seqNames.length; i++) { - var obj = { - name: seqNames[i], - checked: !1 - }; - result.sequences[i] = obj - } - 0 < seqNames.length ? $rootScope.$root.$emit("modal select sequences", result.sequences) : ($rootScope.$root.$emit("modal simple requested", ["Replace With Hi-Res Clips - Warning", "The 'Replace With Hi-Res clips' button replaces lo-res previews with hi-res clips that you have purchased and downloaded.

    There are currently no sequences in your project."]), result.setState(DEFAULT)) - }, - setSequences: function(sequences) { - result.sequences = []; - for (var i = 0; i < sequences.length; i++) sequences[i].checked = !1; - var newArray = []; - newArray.push(sequences[0]); - for (i = 1; i < sequences.length; i++) { - for (var j = 0; j < newArray.length; j++) newArray[j].name === sequences[i].name && (console.log("already exists ", i, j, sequences[i].name), 0, sequences[i].name = sequences[i].name + " (id: " + sequences[i].id + ")"); - newArray.push(sequences[i]) - } - result.sequences = newArray, console.log("ReplaceModel, sequences:", result.sequences), 0 < sequences.length ? $rootScope.$root.$emit("modal select sequences", result.sequences) : ($rootScope.$root.$emit("modal simple requested", ["Replace With Hi-Res Clips - Warning", "The 'Replace With Hi-Res clips' button replaces lo-res previews with hi-res clips that you have purchased and downloaded.

    There are currently no sequences in your project."]), result.setState(DEFAULT)) - }, - setComps: function(comps) { - result.sequences = comps, $rootScope.$root.$emit("modal select comps", result.sequences) - }, - addHires: function(dest, files) { - for (var hiresVO, i = 0; i < files.length; i += 1)(hiresVO = new HiresVO(dest, files[i].fileName)).type = files[i].vs, hiresVO.replace = !0, result.hiresOnFS.push(hiresVO) - } - }; - return result -}]), app.service("SearchModel", ["$rootScope", function($rootScope) { - var result = { - allowInfiniteScroll: !1, - searchResultItems: [], - numOfResults: 0, - onSearch: function(data) { - result.searchResultVO = new ItemsVO(data.commands[0]), result.numOfResults = data.commands[0].nbr_footage + data.commands[0].nbr_music + data.commands[0].nbr_sfx + data.commands[0].nbr_ae, console.log("SearchModel onSearch num of results: ", result.numOfResults), "replace" === result.resultType && (result.searchResultItems = [], window.scrollTo(0, 0), 0 === result.numOfResults ? $rootScope.$emit("message view requested", !0, NO_RESULTS, !0, NO_RESULTS_ICON) : $rootScope.$emit("message view requested", !1)); - for (var i = 0; i < result.searchResultVO.items.length; i++) result.searchResultItems.push(result.searchResultVO.items[i]); - result.isSearching = !1, resizePanel() - }, - sumOfBitmasks: "", - query: "", - filter: "1", - resultType: "replace", - page: 0, - isSearching: !1, - filteredItems: [], - fps: "", - fpsgt: "", - res: "", - pricegt: "", - pricelt: "", - durationgt: "", - durationlt: "" - }; - return result -}]), app.factory("UserModel", [function() { - var firstTimeUser = !0, - user = { - onUserInfo: function(data) { - user.setCredits(data.credit), user.setUserName(data.un), user.setFirstName(data.fn), user.setLastName(data.ln), user.setAvatarURL(data.icon_base, data.av) - }, - setCredits: function(num) { - user.credits = num - }, - getCredits: function() { - return user.credits - }, - setUID: function(uid) { - user.uid = uid - }, - getUID: function() { - return user.uid - }, - setCM: function(cm) { - user.cm = cm - }, - getCM: function() { - return user.cm - }, - setCX: function(cx) { - user.cx = cx - }, - getCX: function() { - return user.cx - }, - setUserName: function(name) { - user.userName = name - }, - getUserName: function() { - return user.userName - }, - setFirstName: function(name) { - user.firstName = name - }, - getFirstName: function() { - return user.firstName - }, - setLastName: function(name) { - user.lastName = name - }, - getLastName: function() { - return user.lastName - }, - setAvatarURL: function(base, url) { - user.avatarURL = base + url - }, - getAvatarURL: function() { - return user.avatarURL - }, - setFirstTimeUser: function(state) { - firstTimeUser = state - }, - getFirstTimeUser: function() { - return firstTimeUser - } - }; - return user -}]), app.factory("VersionsModel", ["$rootScope", function($rootScope) { - var result = { - versions: [], - setVersions: function(v) { - result.versions = []; - for (var i = 0; i < v.length; i++) result.versions[i] = v[i]; - $rootScope.$emit("on versions selected", result.versions) - }, - getVersions: function() { - return result.versions - } - }; - return result -}]), app.factory("ViewStateModel", ["$rootScope", "SearchModel", function($rootScope, SearchModel) { - var state; - return { - allowPreviews: !1, - setState: function(s) { - state = s, SearchModel.allowInfiniteScroll = "search" === state || ($rootScope.$emit("filters button clicked", !1), !1) - }, - getState: function() { - return state - } - } -}]), app.service("AnalyticsService", ["$http", "$rootScope", "UserModel", "CartModel", function($http, $rootScope, UserModel, CartModel) { - var result = { - sendData: function(data, type) { - GA_TRACKING_CODE, - UserModel.getUID(), - UserModel.getUID(), - HOST_NAME, - PLUGIN_VERSION - }, - send: function(payload) { - $http({ - method: "POST", - url: payload - }).then(function(response) { - console.log("AnalyticsService then: ", response) - }, function(response) { - console.log("AnalyticsService error: ", response) - }) - } - }; - return result -}]), app.service("Service", ["$rootScope", "APIService", "LoginModel", "UserModel", "SearchModel", "FreebiesModel", "BinsModel", "ViewStateModel", "DownloadModel", "CheckOutModel", "PreviewsModel", "ReplaceModel", "ViewStateService", "ImportedPreviewsService", "AnalyticsService", "UserService", "BillingInfoModel", function($rootScope, APIService, LoginModel, UserModel, SearchModel, FreebiesModel, BinsModel, ViewStateModel, DownloadModel, CheckOutModel, PreviewsModel, ReplaceModel, ViewStateService, ImportedPreviewsService, AnalyticsService, UserService, BillingInfoModel) { - $rootScope.$on("api call", function(event, apiObj) { - call[apiObj.fn](sortArgs(apiObj.args)) - }); - var call = { - login: function() { - var obj = [{ - command: "login", - username: arguments[0][0], - password: arguments[0][1] - }]; - APIService.call(obj).then(function(data) { - LoginModel.setLoggedIn(!0), LoginModel.setCX(data.commands[0].cx), LoginModel.setCM(data.commands[0].cm), UserService.saveData(data.commands[0].cx, data.commands[0].cm), call.getUserInfo() - }).catch(function(err) {}) - }, - logout: function() { - console.log("Service logout"); - APIService.call([{ - command: "logout" - }]).then(function(data) { - LoginModel.setLoggedIn(!1) - }).catch(function(err) {}) - }, - getUserInfo: function() { - APIService.call([{ - command: "userinfo" - }]).then(function(data) { - "" != data.commands[0].uid && (UserModel.onUserInfo(data.commands[0]), call.getBins(), setTimeout(function() { - call.getCart() - }, 1e3), call.getActiveBin(), call.getBillingAddresses(), LoginModel.getLoggedIn() || LoginModel.setLoggedIn(!0)) - }).catch(function(err) {}) - }, - search: function() { - var obj = [{ - command: "search", - query: SearchModel.query + SearchModel.res + SearchModel.fps + SearchModel.fpsgt + SearchModel.pricegt + SearchModel.pricelt + SearchModel.durationgt + SearchModel.durationlt, - sb: SearchModel.filter, - bm: SearchModel.sumOfBitmasks, - no: "25", - p: SearchModel.page, - col: "1523" - }]; - APIService.call(obj).then(function(data) { - SearchModel.onSearch(data), ViewStateModel.allowPreviews = !0 - }).catch(function(err) {}) - }, - getFreeClips: function() { - APIService.call([{ - command: "get_free_clips" - }]).then(function(data) { - FreebiesModel.onFreebies(data) - }).catch(function(err) {}) - }, - getCart: function() { - APIService.call([{ - command: "get_cart_formatted", - artistinfo: "1" - }]).then(function(data) { - console.log("Service getCart data", data), $rootScope.$emit("on cart", data) - }).catch(function(err) {}) - }, - getCartTotal: function() { - var obj = [{ - command: "get_cart_total", - addressid: BillingInfoModel.getDefaultInfo() ? BillingInfoModel.getDefaultInfo().addressid : "", - use_credits: "1" - }]; - APIService.call(obj).then(function(data) { - $rootScope.$emit("on cart total", data) - }).catch(function(err) {}) - }, - getBillingAddresses: function(setState) { - APIService.call([{ - command: "get_billing_addresses" - }]).then(function(data) { - BillingInfoModel.onBillingInfo(data), setState && $rootScope.$emit("on modal choose billing info requested"), call.getCartTotal() - }).catch(function(err) {}) - }, - setBillingAddress: function(info) { - console.log("Service setBillingAddresses obj:", info); - var data = info[0]; - data.addressID || (data.addressID = ""); - var obj = [{ - command: "set_billing_address", - country: data.country, - addressid: data.addressID, - first_name: data.firstName, - last_name: data.lastName, - company_name: data.organization, - company_department: data.department, - company_id: data.companyID, - vat_id: data.vatID, - street1: data.street1, - street2: data.street2, - city: data.city, - state: data.state, - province: data.province, - postal_code: data.zipCode - }]; - APIService.call(obj).then(function(data) { - call.getBillingAddresses(!0) - }).catch(function(err) {}) - }, - getBins: function() { - APIService.call([{ - command: "get_bins" - }]).then(function(data) { - BinsModel.onBins(data) - }).catch(function(err) {}) - }, - getActiveBin: function() { - APIService.call([{ - command: "get_active_bin" - }]).then(function(data) { - BinsModel.onActiveBin(data) - }).catch(function(err) {}) - }, - setActiveBin: function(id) { - var obj = [{ - command: "set_active_bin", - binid: id - }]; - APIService.call(obj).then(function(data) { - setTimeout(function() { - call.getActiveBin() - }, 1e3) - }).catch(function(err) {}) - }, - getBin: function() { - var obj = [{ - command: "get_bin_formatted", - binid: BinsModel.showBin.id - }]; - APIService.call(obj).then(function(data) { - BinsModel.onBin(data) - }).catch(function(err) {}) - }, - modifyBin: function(binID, addID, rmID) { - var obj = [{ - command: "modify_active_bin", - binid: binID, - addid: addID, - rmid: rmID - }]; - APIService.call(obj).then(function(data) { - "1" == data.commands[0].nbr_removed ? call.getBin(BinsModel.binVO.id) : $rootScope.$emit("added to bin") - }).catch(function(err) {}) - }, - createBin: function(binName) { - var obj = [{ - command: "create_bin", - name: binName - }]; - APIService.call(obj).then(function(data) { - BinsModel.newBinName; - call.setActiveBin(data.commands[0].binid), call.getBins() - }).catch(function(err) {}) - }, - removeBin: function(id) { - var obj = [{ - command: "delete_bin", - binid: id - }]; - APIService.call(obj).then(function(data) { - call.getBins(), $rootScope.$emit("collection removed", data) - }).catch(function(err) {}) - }, - getPurchases: function() { - APIService.call([{ - command: "get_downloads_formatted" - }]).then(function(data) { - console.log("Service getPurchases data", data), $rootScope.$emit("on purchases", data) - }).catch(function(err) {}) - }, - getPurchaseURL: function(itemID, transactionID, versionID, version) { - console.log("Service getPurchaseURL", itemID, transactionID, versionID, version); - var obj = [{ - command: "download", - bid: itemID, - tr: transactionID, - v: versionID, - bitoffset: version - }]; - APIService.call(obj).then(function(data) { - console.log("Service getPurchaseURL data", data), DownloadModel.downloadingBatchURLs ? DownloadModel.onGetAllPurchaseURLs(data) : DownloadModel.onGetPurchaseURL(data) - }).catch(function(err) {}) - }, - modifyCart: function() { - var obj = [{ - command: "modify_active_cart", - addid: arguments[0][0], - rmid: arguments[0][1] - }]; - APIService.call(obj).then(function(data) { - 1 === data.commands[0].nbr_added && $rootScope.$emit("added to cart"), call.getCart(), call.getCartTotal() - }).catch(function(err) {}) - }, - purchaseWithCredits: function(buyAnyway, userData) { - var obj = [{ - command: "purchase_using_credits", - override: buyAnyway, - userdata: userData, - addressid: BillingInfoModel.getDefaultInfo().addressid - }]; - APIService.call(obj).then(function(data) { - console.log("purchaseWithCredits data", data), ReplaceModel.getState() === DEFAULT && $rootScope.$emit("modal simple requested", ["Your purchase has been successful!", "Your items are now ready to download."]), $rootScope.$emit("purchase complete"), ReplaceModel.getState() === NOT_PURCHASED ? call.getPurchases() : ViewStateService.viewRequested("downloads"), call.getUserInfo() - }).catch(function(err) {}) - }, - purchaseWithCash: function(buyAnyway, userData) { - var obj = [{ - command: "purchase_using_cash", - AdobePremierePlugin: "html", - override: buyAnyway, - userdata: userData, - addressid: BillingInfoModel.getDefaultInfo().addressid, - use_credits: "1" - }]; - APIService.call(obj).then(function(data) { - console.log("Service purchaseWithCash data", data), CheckOutModel.onPurchase(data) - }).catch(function(err) {}) - }, - promoRedeem: function(code) { - var obj = [{ - command: "promo_redeem", - promocode: code - }]; - APIService.call(obj).then(function(data) { - call.getUserInfo(), $rootScope.$emit("promo code added", data) - }).catch(function(err) {}) - }, - getImportedPreviews: function() { - console.log("Service getImportedPreviews", ImportedPreviewsService.idsString); - var obj = [{ - command: "get_clip_data_array", - itemids: ImportedPreviewsService.idsString, - col: "1523", - verboselvl: "100" - }]; - APIService.call(obj).then(function(data) { - PreviewsModel.onPreviews(data) - }).catch(function(err) {}) - }, - getFormats: function(item) { - console.log("Service getFormats", item.id); - var obj = [{ - command: "get_versions_formatted", - vm: item.id - }]; - APIService.call(obj).then(function(data) { - console.log("Service getFormats data", data); - var formats = data.commands[0].items; - $rootScope.$emit("formats complete", item, formats) - }).catch(function(err) {}) - }, - getFormatsReplacing: function(item) { - console.log("Service getFormatsReplacing", item.id); - var obj = [{ - command: "get_versions_formatted", - vm: item.id - }]; - APIService.call(obj).then(function(data) { - console.log("Service getFormatsReplacing data", data); - var formats = data.commands[0].items; - $rootScope.$emit("formats replacing complete", item, formats) - }).catch(function(err) {}) - }, - getMissingItems: function(itemIDsString) { - console.log("Service getMissingItems itemIDsString", itemIDsString); - var obj = [{ - command: "get_clip_data_array", - itemids: itemIDsString, - col: "1523", - verboselvl: "100" - }]; - APIService.call(obj).then(function(data) { - ReplaceModel.setState(MISSING_ITEMS), console.log("Service getMissingItems data", data), $rootScope.$emit("missing items complete", data) - }).catch(function(err) {}) - } - }; - return call -}]), app.factory("APIService", ["$http", "ViewStateModel", "LoginModel", function($http, ViewStateModel, LoginModel) { - return { - call: function(data) { - ViewStateModel.allowPreviews = !1; - var url, secret, apiKey, _0xf310 = ["test", "https://test.pond5.com/?page=api", "live", "https://www.pond5.com/?page=api", "oi23Jan3Inwh2io", "220655_769351580"]; - MODE === _0xf310[0] ? API_URL = _0xf310[1] : MODE === _0xf310[2] && (API_URL = _0xf310[3]), API_SECRET = _0xf310[4], API_KEY = _0xf310[5], url = API_URL, secret = API_SECRET, apiKey = API_KEY; - var stringified = JSON.stringify(data), - md5target = stringified + secret + "dragspel", - md5tostring = CryptoJS.MD5(md5target).toString(), - cx = LoginModel.getCX(), - cm = LoginModel.getCM(), - dataObj = { - api_key: apiKey, - commands_json: stringified, - commands_hash: md5tostring, - ver: 1, - https: 1 - }, - jsnstr = JSON.stringify(dataObj); - return $http({ - url: url, - method: "POST", - data: "api=" + jsnstr + "&apicx=" + cx + "&apicm=" + cm, - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - }).then(function(result) { - return ViewStateModel.allowPreviews = !0, result.data - }) - } - } -}]), app.factory("myHttpInterceptor", ["$q", "$rootScope", "ViewStateModel", function($q, $rootScope, ViewStateModel) { - return { - response: function(response) { - var errorFree = !0; - return "POST" === response.config.method && (response.data.e ? (console.log("Apiservice myHttpInterceptor error >>>", response.data), errorFree = !1) : response.data.commands && response.data.commands.forEach(function(entry) { - if (entry && entry.hasOwnProperty("e")) { - if (response.config.data && -1 != response.config.data.indexOf("userinfo")) console.log("myHttpInterceptor user info, do not show alert ", response); - else if (103 === response.data.commands[0].c) response.data.commands[0].a && (console.log("APIService myHttpInterceptor alreadyBought or onwClips", response.data.commands[0].a), 0 < response.data.commands[0].a.bought_before.length && ($rootScope.$emit("alreadyBought", response.data.commands[0].a.bought_before), console.log("APIService myHttpInterceptor alreadyBought", response.data.commands[0].a.bought_before)), 0 < response.data.commands[0].a.ownClips.length && ($rootScope.$emit("ownClips", response.data.commands[0].a.ownClips), console.log("APIService myHttpInterceptor ownClips", response.data.commands[0].a.ownClips))); - else { - console.log("myHttpInterceptor modal simple requested :", entry), "You are not logged in" == entry.s.split(": ")[1] ? $rootScope.$emit("modal not logged in", [ERROR]) : $rootScope.$emit("modal simple requested", [ERROR, entry.s.split(": ")[1]]) - } - errorFree = !1 - } - })), errorFree ? response : $q.reject(response) - }, - responseError: function(response) { - return response.config.url == MODAL_INTRO_HTML || response.config.url == MODAL_CHOOSE_BILLING_INFO_HTML ? console.log("apiService don't show error modal for ", response.config.url) : ($rootScope.$emit("modal simple requested", [ERROR, response.headers().status]), console.log("apiService don't show error modal but response ", response)), $q.reject(response) - } - } -}]), app.config(function($httpProvider) { - $httpProvider.interceptors.push("myHttpInterceptor") -}), app.service("CheckOutService", ["CartModel", "UserModel", "Service", function(CartModel, UserModel, Service) { - this.onCheckOutRequested = function(buyAnyway) { - console.log("CheckOutService total before VAT: ", CartModel.cartTotal.subtotals.afterVat), console.log("CheckOutService credits: ", CartModel.cartTotal.creditsData.availableSum), console.log("CheckOutService buyAnyway: ", buyAnyway), CartModel.cartTotal.creditsData.availableSum < CartModel.cartTotal.subtotals.afterVat ? Service.purchaseWithCash(buyAnyway) : Service.purchaseWithCredits(buyAnyway) - } -}]), app.service("CreateOnFileSystemService", ["AppModel", "CreateFileCompleteService", function(AppModel, CreateFileCompleteService) { - var call = { - createUserHomeFolder: function() { - call.createDir(AppModel.getDirHomePond5()) - }, - createUserSubFolders: function() { - console.log("CreateOnFileSystemService createUserSubFolders", AppModel.getDirDefaultLib()); - for (var dirs = [AppModel.getDirImports(), AppModel.getDirPrefs(), AppModel.getDirDefaultLib(), AppModel.getDirDestinations(), AppModel.getDirUser()], i = 0; i < dirs.length; i++) { - var dir = dirs[i]; - call.createDir(dir) - } - }, - createDestinationBaseFolder: function() { - call.createDir(AppModel.currentBaseFolder + path.sep + "pond5", !0) - }, - createDestinationFolders: function() { - AppModel.previewsDir = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + "previews", AppModel.purchasedDir = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + "purchased", call.createDir(AppModel.previewsDir), call.createDir(AppModel.purchasedDir) - }, - createDir: function(dir, isDestination) { - fs.exists(dir, function(exists) { - exists ? call.onDirReady(dir, isDestination) : fs.mkdir(dir, 511, function(err) { - if (err) throw err; - call.onDirReady(dir, isDestination) - }) - }) - }, - onDirReady: function(dir, isDestination) { - if (isDestination = isDestination || !1) this.createDestinationFolders(); - else { - var filePath, xml; - switch (dir) { - case AppModel.getDirHomePond5(): - call.createUserSubFolders(); - break; - case AppModel.getDirImports(): - filePath = "imported_previews.xml", xml = ''; - break; - case AppModel.getDirPrefs(): - filePath = "preferences.xml", xml = ''; - break; - case AppModel.getDirUser(): - filePath = "user.xml", xml = ''; - break; - case AppModel.getDirDestinations(): - filePath = "destinations.xml", xml = ''; - break; - case AppModel.getDirDefaultLib(): - filePath = "defaultLib.xml", xml = ''; - break; - case AppModel.currentBaseFolder: - this.createDestinationFolders(); - break; - default: - return - } - filePath && call.createFile(dir + path.sep + filePath, '' + xml) - } - }, - createFile: function(file, content) { - fs.exists(file, function(exists) { - exists ? CreateFileCompleteService.onFileReady(file) : fs.writeFile(file, content, function(err) { - if (err) throw err; - console.log("CreateOnFileSystemService, created file: ", file), CreateFileCompleteService.onFileReady(file) - }) - }) - } - }; - return call -}]), app.service("DeleteOnFileSystemService", [function() { - return { - deleteFiles: function(items) { - items.forEach(function(item) { - var file = item.downloadDestination + item.fileName; - fs.exists(file, function(exists) { - exists && fs.unlink(file, function(err) { - if (err) throw err - }) - }) - }) - }, - deleteFolder: function(folders, cb) { - console.log("DeleteOnFileSystemService deleteFolder, folders, length:", folders.length), folders.forEach(function(folder) { - console.log("DeleteOnFileSystemService deleteFolder, folder:", folder), fs.exists(folder, function(exists) { - exists ? rimraf(folder, function(err) { - if (err) throw err; - console.log("DeleteOnFileSystemService deleteFolder deleted: ", folder), cb() - }) : (console.log("DeleteOnFileSystemService deleteFile folder does not exist:", folder), cb()) - }) - }) - } - } -}]), app.factory("DownloadBatchService", ["Service", "PurchasesModel", "DownloadModel", function(Service, PurchasesModel, DownloadModel) { - return { - onBatchRequested: function(purchases) { - var j, i; - for (purchases = purchases || PurchasesModel.purchasesVO.items, i = 0; i < purchases.length; i += 1) - for (j = 0; j < PurchasesModel.purchasesVO.items.length; j += 1) purchases[i].id == PurchasesModel.purchasesVO.items[j].id && (purchases[i] = PurchasesModel.purchasesVO.items[j]); - for (DownloadModel.downloadingBatchURLs = !0, purchases = purchases.filter(function(v, i, a) { - return a.indexOf(v) == i - }), i = 0; i < purchases.length; i += 1) Service.getPurchaseURL(purchases[i].id, purchases[i].transactionID, purchases[i].versionID, purchases[i].version) - } - } -}]), app.service("DownloadCancelService", ["$rootScope", "DeleteOnFileSystemService", "ProgressService", "DownloadModel", function($rootScope, DeleteOnFileSystemService, ProgressService, DownloadModel) { - return { - onCancelSingle: function(item) { - console.log("DownloadCancelService onCancelSingle: ", item, item.downloadType), item.canceled = !0, $rootScope.$emit("cancel download", item), ProgressService.clearItem(item), DeleteOnFileSystemService.deleteFiles([item]), item.downloading && (item.downloading = !1, DownloadModel.downloadCounter--); - for (var len = DownloadModel.itemsDownloadList.length; len--;) - if (DownloadModel.itemsDownloadList[len].fileName === item.fileName) { - var removal = DownloadModel.itemsDownloadList[len]; - DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { - return itm !== removal - }) - } console.log("DownloadCancelService onCancelSingle num of items: ", DownloadModel.itemsDownloadList.length), $rootScope.$emit("modal simple requested", ["", "Download of " + item.fileName + " has been canceled."], "sm") - }, - onCancelAll: function() { - console.log("DownloadCancelService cancel all downloads", DownloadModel.itemsDownloadList); - for (var len = DownloadModel.itemsDownloadList.length; len--;) { - var item = DownloadModel.itemsDownloadList[len]; - 100 !== item.progressPerc && (item.canceled = !0, $rootScope.$emit("cancel download", item), ProgressService.clearItem(item), DeleteOnFileSystemService.deleteFiles([item])) - } - $rootScope.$emit("modal simple requested", ["", "All incomplete downloads have been canceled and deleted."], "sm"), DownloadModel.downloadCounter = -1, DownloadModel.itemsDownloadList = [] - } - } -}]), app.service("DownloadCompleteService", ["$rootScope", "UnzipService", function($rootScope, UnzipService) { - return { - onComplete: function(items) { - UnzipService.unzipItems(items) - } - } -}]), app.service("DownloadRequestService", ["$rootScope", "DownloadService", "ProgressService", "DownloadModel", "ReplaceModel", "AppModel", "ImportService", "ReplaceService", "StayAwakeService", "UnzipService", function($rootScope, DownloadService, ProgressService, DownloadModel, ReplaceModel, AppModel, ImportService, ReplaceService, StayAwakeService, UnzipService) { - $rootScope.$on("download requested", function(event, items) { - var downloadFolderName; - console.log("DownloadRequestService DownloadModel.itemsDownloadList: ", DownloadModel.itemsDownloadList), "preview" === items[0].downloadType ? downloadFolderName = "previews" : "purchase" === items[0].downloadType && (downloadFolderName = "purchased"); - var item, dest = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + downloadFolderName + path.sep; - console.log("DownloadRequestService downloadRequested items:", items), $rootScope.$emit("scroll progress to top"); - for (var i = 0; i < items.length; i++) { - var codec; - (item = items[i]).downloadDestination = dest, "preview" === item.downloadType ? "Video" == item.type || "AE" == item.type ? item.downloadURL = item.h264URL : "Sound effect" == item.type || "Music" == item.type ? item.downloadURL = item.m4aURL : "Photo" != item.type && "Illustration" != item.type || (item.downloadURL = item.iconLargeURL) : "purchase" === item.downloadType && (item.downloadURL = item.hiresURL), "Photo" == item.type ? item.ext = "jpg" : item.ext = item.downloadURL.substr(item.downloadURL.lastIndexOf(".") + 1).split("?")[0], item.videoCodec && (codec = item.videoCodec), "preview" !== item.downloadType && "unknown" !== codec && void 0 !== codec || (codec = ""), item.fileName = getFormattedName(item.id + " " + codec + " " + item.name + "." + item.ext), item.progressName = getAbbrName(item.fileName, 20), "preview" === item.downloadType && "AE" === item.vs && (item.fileName = "AE " + item.fileName), "purchase" === item.downloadType && ("AE" === item.vs ? item.fileName = "AE " + item.fileName : item.fileName = "hires " + item.fileName), $rootScope.$emit("open progress", !1), item.progressPerc = "", item.progressMB = "", ProgressService.addItem(item) - } - $rootScope.$$listenerCount["on item downloaded"] || $rootScope.$on("on item downloaded", function(event) { - DownloadModel.downloadCounter++, console.log("DownloadRequestService on item downloaded DownloadModel.downloadCounter: ", DownloadModel.downloadCounter), console.log("DownloadRequestService on item downloaded DownloadModel.itemsDownloadList: ", DownloadModel.itemsDownloadList); - var item = DownloadModel.itemsDownloadList[DownloadModel.downloadCounter]; - if (item) { - StayAwakeService.updateState(!0); - new DownloadService.download(item) - } else if (StayAwakeService.updateState(!1), DownloadModel.downloadCounter--, console.log("DownloadRequestService download complete, check if something needs to be done, complete previews", ProgressService.getCompletedPreviews()), ProgressService.getCompletedPreviewsStatus() && ImportService.importClips(ProgressService.getCompletedPreviews()), ProgressService.getCompletedPurchasesStatus()) { - console.log("DownloadRequestService purchases completed: ", ProgressService.getCompletedPurchases()), console.log("DownloadRequestService purchases completed ReplaceModel.getState(): ", ReplaceModel.getState()); - var AEItems = []; - if (ProgressService.getCompletedPurchases().forEach(function(item) { - "AE" == item.type && AEItems.push(item) - }), "1.0.8" != PLUGIN_VERSION && UnzipService.unzipItems(AEItems), ReplaceModel.getState() === NOT_DOWNLOADED) { - var dest = AppModel.currentBaseFolder + path.sep + "pond5" + path.sep + "purchased" + path.sep; - ProgressService.getCompletedPurchases().forEach(function(entry) { - ReplaceModel.addHires(dest, [entry]) - }), ReplaceService.onPurchasedAndDownloaded(AEItems.length) - } - } - }), console.log("DownloadRequestService new request, ProgressService.getIncompleteItems ", ProgressService.getIncompleteItems()), 0 < ProgressService.getIncompleteItems().length && !ProgressService.getDownloadingStatus() && $rootScope.$emit("on item downloaded") - }) -}]), app.service("DownloadService", ["$rootScope", "ProgressService", function($rootScope, ProgressService) { - function download(item) { - console.log("DownloadService download item: ", item); - var allowWriting = !0; - $rootScope.$on("cancel download", function(event, itm) { - itm.fileName === item.fileName && (itm.canceled = !0, item.canceled = !0, allowWriting = !1) - }), item.downloading = !0; - var file, sizeOnFS, writeOptions, path = item.downloadDestination + item.fileName; - writeOptions = fs.existsSync(path) ? (sizeOnFS = fs.statSync(path).size, console.log("DownloadService sizeOnFS: ", sizeOnFS), { - flags: "r+" - }) : (console.log("DownloadService file does not exist yet, create stream"), { - flags: "w" - }), file = fs.createWriteStream(path, writeOptions), https.get(item.downloadURL, function(res) { - var len; - res.headers["content-length"] ? (len = parseInt(res.headers["content-length"], 10), console.log("DownloadService res has content-length: ", res)) : console.log("DownloadService content-length unknown", res); - var progressPerc, cur = 0, - total = len / 1048576; - - function setToComplete() { - item.canceled || (item.progressPerc = 100, item.progressMB = total.toFixed(2) + "/" + total.toFixed(2) + "MB", item.completed = !0), item.canceled = !1, item.downloading = !1, $rootScope.$emit("on item downloaded"), $rootScope.$digest() - } - res.pipe(file), len <= sizeOnFS && (file.end(), setToComplete()), res.on("data", function(chunk) { - allowWriting ? (cur += chunk.length, progressPerc = (100 * cur / len).toFixed(2), $rootScope.$apply(function() { - item.progressPerc = progressPerc.split(".")[0], item.progressMB = (cur / 1048576).toFixed(2) + "/" + total.toFixed(2) + "MB" - })) : res.destroy() - }).on("error", function(e) { - console.log("DownloadService error: " + e.message) - }).on("end", function() { - file.end(), setToComplete() - }) - }).on("error", function(err) { - console.error("Download Error code and filename:", err.code, item.fileName), console.error("Download err:", err), item.progressPerc = 0, item.progressMB = "", setTimeout(function() { - download(item, options) - }, 1e3) - }) - } - return { - download: function(item, options) { - return new download(item, options) - } - } -}]), app.service("ImportAEService", ["$rootScope", "ReplaceModel", function($rootScope, ReplaceModel) { - var call = { - showingModal: !1, - import: function(sourceDir) { - var walk = function(dir, done) { - var files = []; - fs.readdir(dir, function(err, list) { - if (err) return done(err); - var i = 0; - ! function next() { - var file = list[i++]; - if (!file) return done(null, files); - file = dir + "/" + file, fs.stat(file, function(err, stat) { - stat && stat.isDirectory() ? walk(file, function(err, res) { - files = files.concat(res), next() - }) : (files.push(file), next()) - }) - }() - }) - }; - walk(sourceDir, function(err, files) { - if (err) throw err; - for (var i = 0; i < files.length; i += 1) console.log("ImportService file", files[i]), -1 != files[i].indexOf(".aep") && csInterface.evalScript("importAETemplate(" + JSON.stringify(files[i]) + ")", function(result) { - call.showingModal || ($rootScope.$emit("modal simple requested", ["", "Your project has been updated."]), call.showingModal = !0), console.log("ImportAEService import showingModal", call.showingModal) - }) - }) - } - }; - return call -}]), app.factory("ImportedPreviewsService", ["$rootScope", function($rootScope) { - var result = { - readXML: function() { - var dest = path.sep + "pond5" + path.sep + "imports" + path.sep + "imported_previews.xml"; - result.file = getUserHome() + dest, fs.readFile(result.file, "utf8", function(err, data) { - if (err) throw err; - result.xml = data, result.parseXML() - }) - }, - saveItem: function(id) { - var idsString = result.idsString.toString(); - 1 == idsString.indexOf(id.toString()) && (0 < idsString.length ? result.idsString += "," + id : result.idsString = id, result.writeToDisk()) - }, - deleteItem: function(id) { - -1 != result.idsString.indexOf(id) && (result.idsString = result.idsString.replace(id, "")), "," == result.idsString.substr(0, 1) && (result.idsString = result.idsString.substr(1)), "," == result.idsString.substr(result.idsString.length - 1, result.idsString.length) && (result.idsString = result.idsString.slice(0, -1)), result.writeToDisk(), $rootScope.$emit("api call", { - fn: "getImportedPreviews" - }) - }, - parseXML: function() { - var parser = new xml2js.Parser; - parser.addListener("end", function(res) { - (result.parsedXML = res) && (result.idsString = res.root.previews[0].$.ids) - }), parser.parseString(result.xml) - }, - writeToDisk: function() { - result.parsedXML.root.previews[0].$.ids = result.idsString; - var xml = (new xml2js.Builder).buildObject(result.parsedXML); - fs.writeFile(result.file, xml, function(err) { - if (err) throw err - }) - } - }; - return result -}]), app.service("MissingItemsService", ["$rootScope", "MissingItemsModel", "ReplaceModel", "Service", "CartModel", "ReplaceServiceShared", function($rootScope, MissingItemsModel, ReplaceModel, Service, CartModel, ReplaceServiceShared) { - $rootScope.$on("missing items complete", function(event, items) { - console.log("MissingItemsService on missing items: ", items), ReplaceModel.getState() === MISSING_ITEMS && result.onMissingItems(items) - }), $rootScope.$on("formats replacing complete", function(event, item, formats) { - ReplaceModel.getState() === MISSING_ITEMS && result.onMissingItemsFormats(item, formats) - }), $rootScope.$on("on purchases vo", function(event, vo) { - console.log("MissingItemsService on purchases vo, state: ", ReplaceModel.getState()), ReplaceModel.getState() != DEFAULT && result.onPurchasesVO(vo) - }); - var result = { - missingItemsCounter: 0, - onMissingItems: function(data) { - var missingItemsVO = new MissingItemsVO(data.commands[0]); - (MissingItemsModel.missingItemsVO = missingItemsVO).items.forEach(function(entry) { - Service.getFormatsReplacing(entry) - }) - }, - onMissingItemsFormats: function(item, formats) { - if (result.missingItemsCounter++, 1 < (formats = _.uniq(formats, function(p) { - return p.ti - })).length) - for (i = 0; i < formats.length; i++) item.formats[i] = new ItemVO(formats[i]), item.parentFormatID = item.id, item.formats[i].offset = formats[i].offset; - result.missingItemsCounter === MissingItemsModel.missingItemsVO.items.length && (result.missingItemsCounter = 0, Service.getPurchases()) - }, - onPurchasesVO: function(purchasesVO) { - for (var item, missingItems = MissingItemsModel.missingItemsVO.items, cartItems = CartModel.cartVO.items, purchasedItems = purchasesVO.items, i = 0; i < missingItems.length; i++) { - var cartItem, purchase; - item = missingItems[i]; - for (var j = 0; j < cartItems.length; j++) { - cartItem = cartItems[j], item.id == cartItem.id && (item.inCart = !0); - for (var formats = item.formats, k = 0; k < formats.length; k++) formats[k].id == cartItem.id && formats[k].offset == cartItem.offset && (formats[k].inCart = !0, item.inCart = !0) - } - for (j = 0; j < purchasedItems.length; j++) { - purchase = purchasedItems[j], item.id == purchase.id && (item.inDownloads = !0, item.transactionID = purchase.transactionID); - for (formats = item.formats, k = 0; k < formats.length; k++) formats[k].id == purchase.id && (formats[k].inDownloads = !0, formats[k].transactionID = purchase.transactionID, purchasedItems[j].parentFormatID && (formats[k].parentFormatID = purchase.parentFormatID)) - } - } - ReplaceModel.getState() === MISSING_ITEMS ? $rootScope.$emit("modal replace", missingItems) : ReplaceModel.getState() === NOT_PURCHASED && ReplaceServiceShared.onPurchased(missingItems) - } - }; - return result -}]), app.service("ProgressService", ["$rootScope", "DownloadModel", function($rootScope, DownloadModel) { - var result = { - alreadyHasItem: function(item) { - var itemsContainItem = !1; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.fileName === item.fileName && (itemsContainItem = !0) - }), itemsContainItem - }, - addItem: function(item) { - DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.fileName === item.fileName && (console.log("ProgressService already in list: ", item.fileName), item.completed = !1, item.imported = !1, item.canceled = !1, item.progressPerc = 0, item.progressMB = "", DownloadModel.downloadCounter--, result.clearItem(item), console.log("ProgressService already in list, cleared: ", DownloadModel.itemsDownloadList)) - }), DownloadModel.itemsDownloadList.push(item), console.log("ProgressService addItem, list: ", DownloadModel.itemsDownloadList), $rootScope.$emit("added to progress") - }, - clearCompleteItems: function() { - console.log("ProgressService clearCompleteItems "); - for (var len = DownloadModel.itemsDownloadList.length, oldLen = len; len--;) { - var item = DownloadModel.itemsDownloadList[len]; - if (100 === item.progressPerc) { - item.completed = !1, item.imported = !1, item.canceled = !1, item.progressPerc = 0; - var removal = DownloadModel.itemsDownloadList[len]; - DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { - return itm !== removal - }) - } - } - var diff = oldLen - DownloadModel.itemsDownloadList.length; - DownloadModel.downloadCounter = DownloadModel.downloadCounter - diff, console.log("ProgressService clearCompleteItems DownloadModel.itemsDownloadList: ", DownloadModel.itemsDownloadList), console.log("ProgressService clearCompleteItems new downloadCounter: ", DownloadModel.downloadCounter), $rootScope.$emit("clear progress") - }, - clearIncompleteItems: function() { - console.log("ProgressService clearIncompleteItems "); - for (var len = DownloadModel.itemsDownloadList.length; len--;) - if (100 !== DownloadModel.itemsDownloadList[len].progressPerc) { - var removal = DownloadModel.itemsDownloadList[len]; - DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { - return itm !== removal - }) - } $rootScope.$emit("on clear", DownloadModel.itemsDownloadList) - }, - clearAllItems: function() { - console.log("ProgressService clearAllItems "), DownloadModel.itemsDownloadList = [], $rootScope.$emit("clear progress"), DownloadModel.downloadCounter = 0 - }, - clearItem: function(item) { - console.log("ProgressService clearItem "); - for (var len = DownloadModel.itemsDownloadList.length; len--;) - if (DownloadModel.itemsDownloadList[len].fileName === item.fileName) { - var removal = DownloadModel.itemsDownloadList[len]; - DownloadModel.itemsDownloadList = DownloadModel.itemsDownloadList.filter(function(itm) { - return itm !== removal - }) - } $rootScope.$emit("clear progress") - }, - getIncompleteItems: function() { - var incompletes = []; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.completed || (console.log("ProgressService not completed: ", entry.fileName), incompletes.push(entry)) - }), incompletes - }, - getCompletedPreviewsStatus: function() { - var allCompleted = !0; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.completed || "preview" !== entry.downloadType || (allCompleted = !1) - }), 0 === DownloadModel.itemsDownloadList.length && (allCompleted = !1), console.log("ProgressService getCompletedPreviewsStatus allCompleted", allCompleted), allCompleted - }, - getCompletedPreviews: function() { - var completes = []; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.completed && "preview" == entry.downloadType && completes.push(entry) - }), completes - }, - getCompletedPurchasesStatus: function() { - var allCompleted = !0; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.completed || "purchase" !== entry.downloadType || (allCompleted = !1) - }), 0 === DownloadModel.itemsDownloadList.length && (allCompleted = !1), console.log("ProgressService getCompletedPurchasesStatus allCompleted", allCompleted), allCompleted - }, - getCompletedPurchases: function() { - var completes = []; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.completed && "purchase" == entry.downloadType && completes.push(entry) - }), completes - }, - getDownloadingStatus: function() { - var downloading = !1; - return DownloadModel.itemsDownloadList.forEach(function(entry) { - entry.downloading && (downloading = !0) - }), downloading - } - }; - return result -}]), app.service("ReadClipsOnFSService", ["$rootScope", "ReplaceModel", "MissingItemsModel", "ViewStateService", "DownloadBatchService", "AppModel", function($rootScope, ReplaceModel, MissingItemsModel, ViewStateService, DownloadBatchService, AppModel) { - var call = { - listPurchasesOnFS: function(cb) { - ReplaceModel.hiresOnFS = []; - for (var cbCounter = 0, i = 0; i < AppModel.baseFolders.length; i++) call.readPurchasesFolders(AppModel.baseFolders[i] + path.sep + "pond5" + path.sep + "purchased" + path.sep, function() { - ++cbCounter === AppModel.baseFolders.length && (console.log("\nReadClipsOnFSService ReplaceModel.hiresOnFS done: ", cbCounter, ReplaceModel.hiresOnFS), call.listPreviewsOnFS(function() { - cb() - })) - }) - }, - readPurchasesFolders: function(dest, cb) { - fs.readdir(dest, function(err, files) { - if (err) throw new Error("ReadClipsOnFSService: " + dest + " does not exist."); - var hiresVO; - files = files.filter(junk.not); - for (var i = 0; i < files.length; i += 1) hiresVO = new HiresVO(dest, files[i]), ReplaceModel.hiresOnFS.push(hiresVO), 0 === path.extname(files[i]).length ? hiresVO.type = "AE folder" : ".zip" === path.extname(files[i]) ? hiresVO.type = "AE zip" : ".mov" === path.extname(files[i]) ? hiresVO.type = "video" : ".wav" === path.extname(files[i]) && (hiresVO.type = "audio"); - cb() - }) - }, - listPreviewsOnFS: function(cb) { - ReplaceModel.previewsOnFS = []; - for (var i = 0; i < AppModel.baseFolders.length; i++) { - var walk = function(dir, done) { - var files = []; - fs.readdir(dir, function(err, list) { - if (err) return done(err); - var i = 0; - ! function next() { - var file = list[i++]; - if (!file) return done(null, files); - file = dir + "/" + file, fs.stat(file, function(err, stat) { - stat && stat.isDirectory() ? walk(file, function(err, res) { - files = files.concat(res), next() - }) : (files.push(file), next()) - }) - }() - }) - }, - dest = AppModel.baseFolders[i] + path.sep + "pond5" + path.sep + "previews", - counter = 0; - walk(dest, function(err, files) { - if (err) throw err; - for (var previewVO, i = 0; i < files.length; i += 1) previewVO = new PreviewVO(dest, files[i]), ReplaceModel.previewsOnFS.push(previewVO); - ++counter === AppModel.baseFolders.length && cb() - }) - } - } - }; - return call -}]), app.service("ReplaceServiceShared", ["$rootScope", "ReplaceModel", "Service", "MissingItemsModel", "ViewStateService", "DownloadBatchService", "ImportAEService", "DeleteOnFileSystemService", function($rootScope, ReplaceModel, Service, MissingItemsModel, ViewStateService, DownloadBatchService, ImportAEService, DeleteOnFileSystemService) { - var call = { - removeDuplicates: function(clips) { - return clips = clips.filter(function(v, i, a) { - return a.indexOf(v) === i - }) - }, - getPreviewsOnFSNames: function() { - var previewNamesonFS = []; - return ReplaceModel.previewsOnFS.forEach(function(entry) { - previewNamesonFS.push(entry.name) - }), previewNamesonFS - }, - filterNonP5Clips: function(clips, previewNamesOnFS) { - return clips = clips.filter(function(n) { - return -1 != previewNamesOnFS.indexOf(n) - }) - }, - getPreviewsIDs: function(clips) { - var previewIDs = []; - return clips.forEach(function(entry) { - var substr = entry.split(" "); - "AE" === substr[0] ? previewIDs.push(substr[1]) : previewIDs.push(substr[0]) - }), console.log("\nReplaceServiceShared previewIDs: " + previewIDs), previewIDs - }, - setReplaceProp: function(ids) { - for (var i = 0; i < ids.length; i++) - for (var j = 0; j < ReplaceModel.hiresOnFS.length; j++) ids[i] === ReplaceModel.hiresOnFS[j].id && (ReplaceModel.hiresOnFS[j].replace = !0) - }, - getMissingItemIDs: function(clipsInSeqs) { - var clipsInSelectedSequences = clipsInSeqs; - console.log("ReplaceService ReplaceModel.aeItemsinProjectView: ", ReplaceModel.getAEItems()), 0 < ReplaceModel.getAEItems().length && (clipsInSelectedSequences = clipsInSelectedSequences.concat(ReplaceModel.getAEItems())), console.log("ReplaceService clips after concat layer items and AE items: ", clipsInSelectedSequences), clipsInSelectedSequences = call.removeDuplicates(clipsInSelectedSequences), console.log("\nReplaceServiceShared clipsInSelectedSequences after removing duplicates: ", clipsInSelectedSequences); - var previewNamesonFS = call.getPreviewsOnFSNames(); - console.log("\nReplaceServiceShared previewNamesonFS: ", previewNamesonFS), clipsInSelectedSequences = call.filterNonP5Clips(clipsInSelectedSequences, previewNamesonFS), console.log("\nReplaceServiceShared after filterNonP5Clips", clipsInSelectedSequences); - var previewIDs = call.getPreviewsIDs(clipsInSelectedSequences); - console.log("\nReplaceServiceShared previewIDs: " + previewIDs), call.setReplaceProp(previewIDs), console.log("\nReplaceServiceShared after set replace: " + ReplaceModel.hiresOnFS); - var hiresIDs = call.getHiresIDsonFS(); - console.log("\nReplaceServiceShared hiresIDs: " + hiresIDs); - var missingItemIDs = _(previewIDs).difference(hiresIDs), - missingIDsToString = missingItemIDs.join(","); - if (console.log("nReplaceServiceShared missingIDsToString: " + missingIDsToString), 0 < missingItemIDs.length) Service.getMissingItems(missingIDsToString); - else { - if (0 < hiresIDs.length) return hiresIDs.length; - 0 === clipsInSelectedSequences.length && (ReplaceModel.setState(DEFAULT), $rootScope.$emit("modal simple requested", ["", "There are no Pond5 previews in your current project."])) - } - }, - getHiresIDsonFS: function() { - var hiresIDs = []; - return ReplaceModel.hiresOnFS.forEach(function(entry) { - (entry.replace || entry.importAE) && hiresIDs.push(entry.id) - }), hiresIDs - }, - onModalReplaceOK: function() { - for (var item, missingItems = MissingItemsModel.missingItemsVO.items, itemsNotPurchased = [], itemsNotDownloaded = [], i = 0; i < missingItems.length; i++)(item = missingItems[i]).selected && !item.inDownloads && itemsNotPurchased.push(item), item.selected && item.inDownloads && itemsNotDownloaded.push(item); - 0 < itemsNotPurchased.length ? call.onNotPurchased(itemsNotPurchased) : 0 < itemsNotDownloaded.length ? (console.log("ReplaceServiceShared onModalReplaceOK, download items: ", itemsNotDownloaded), ReplaceModel.missingDownloads = itemsNotDownloaded, call.onNotDownloaded(itemsNotDownloaded)) : (ReplaceModel.setState(PURCHASED_AND_DOWNLOADED), console.log("ReplaceServiceShared onModalReplaceOK, replace"), call.onPurchasedAndDownloaded()) - }, - onNotPurchased: function(itemsNotPurchased) { - for (var addToCartItems = [], i = 0; i < itemsNotPurchased.length; i++) - if (item = itemsNotPurchased[i], 0 < itemsNotPurchased[i].formats.length) - for (var j = 0; j < itemsNotPurchased[i].formats.length; j++) format = itemsNotPurchased[i].formats[j], format.selected && (console.log("ReplaceServiceShared onNotPurchased add this format to cart: ", format), addToCartItems.push(format.id)); - else console.log("ReplaceServiceShared onNotPurchased add this item to cart: ", item), addToCartItems.push(item.id); - $rootScope.$emit("modal simple requested", ["", "Please review your Cart. Press the 'Checkout' button to proceed with replacing your previews."]); - var apiObj = { - fn: "modifyCart", - args: [addToCartItems.join(","), ""] - }; - $rootScope.$emit("api call", apiObj), ViewStateService.viewRequested("cart"), ReplaceModel.setState(NOT_PURCHASED) - }, - onPurchased: function(downloadItems) { - console.log("ReplaceServiceShared onPurchased: ", downloadItems); - for (var item, missingItems = MissingItemsModel.missingItemsVO.items, itemsNotDownloaded = [], i = 0; i < missingItems.length; i++)(item = missingItems[i]).inDownloads && itemsNotDownloaded.push(item); - 0 < itemsNotDownloaded.length && (console.log("ReplaceServiceShared onPurchased, download items: ", itemsNotDownloaded), ReplaceModel.missingDownloads = itemsNotDownloaded, $rootScope.$emit("modal simple requested", ["Your purchase has been successful.", "Your purchased clips will begin downloading now. Once the downloads are completed, your lo-res previews will be replaced with your high-res clips."]), call.onNotDownloaded(itemsNotDownloaded, !0)) - }, - onNotDownloaded: function(itemsNotDownloaded, afterPurchase) { - afterPurchase = afterPurchase || !1, console.log("ReplaceServiceShared onNotDownloaded missing items:", itemsNotDownloaded); - for (var downloadItems = [], i = 0; i < itemsNotDownloaded.length; i++) - if (item = itemsNotDownloaded[i], 0 < itemsNotDownloaded[i].formats.length) - for (var j = 0; j < itemsNotDownloaded[i].formats.length; j++) format = itemsNotDownloaded[i].formats[j], format.selected && (console.log("ReplaceServiceShared onNotDownloaded download this format: ", format), downloadItems.push(format)); - else console.log("ReplaceServiceShared onNotDownloaded download item: ", item), downloadItems.push(item); - afterPurchase || $rootScope.$emit("modal simple requested", ["You have purchases that are missing in your project. ", "They will be downloaded. Once the downloads are completed, your lo-res previews will be replaced with your high-res clips."]), DownloadBatchService.onBatchRequested(downloadItems), ReplaceModel.setState(NOT_DOWNLOADED) - } - }; - return call -}]), app.service("ScrollService", ["SearchModel", "Service", function(SearchModel, Service) { - this.onScroll = function() { - if (SearchModel.allowInfiniteScroll) { - var m = document.getElementById("main-holder"); - 1 === (getScroll()[1] - 72) / (m.scrollHeight - window.innerHeight) && (console.log("ScrollService show more: " + SearchModel.isSearching), SearchModel.isSearching || (SearchModel.isSearching = !0, SearchModel.resultType = "add", SearchModel.page = SearchModel.page + 1, Service.search())) - } - } -}]), app.factory("StartUpService", ["$rootScope", "CreateOnFileSystemService", "MissingItemsService", "ViewStateService", "AppModel", function($rootScope, CreateOnFileSystemService, MissingItemsService, ViewStateService, AppModel) { - return $("#logo").click(function() { - location.reload() - }), $rootScope.$on("environment set", function() { - console.log("StartUpService, 26/10 pointing at ", window.location.href), gup("tp", window.location.href) && (THIRD_PARTY = gup("tp", window.location.href)), -1 < window.location.href.indexOf("test") ? MODE = "test" : MODE = "live", console.log("StartUpService MODE:", MODE), console.log("StartUpService OS:", os.platform()), console.log("StartUpService, app version: ", PLUGIN_VERSION), AppModel.currentBaseFolder = AppModel.getDocumentsPath(), console.log("StartUpService currentBaseFolder: ", AppModel.currentBaseFolder + "\n\n"), CreateOnFileSystemService.createUserHomeFolder(), MissingItemsService.missingItemsCounter = 0, ViewStateService.viewRequested("search") - }), { - init: function() { - setTimeout(function() { - AppModel.setEnv() - }, 2e3) - } - } -}]), app.factory("StayAwakeService", ["$rootScope", "DownloadModel", function($rootScope, DownloadModel) { - return { - updateState: function(state) { - console.log("StayAwakeService state: ", state), state && !DownloadModel.stayAwake ? (sleep.prevent(), DownloadModel.stayAwake = !0) : !state && DownloadModel.stayAwake && (sleep.allow(), DownloadModel.stayAwake = !1) - } - } -}]), app.service("TransactionService", ["$q", "ViewStateService", "Service", "ReplaceModel", "AnalyticsService", "CartModel", function($q, ViewStateService, Service, ReplaceModel, AnalyticsService, CartModel) { - this.onMessageReceivedFromAdyen = function(event) { - console.log("event.source: ", event.source), console.log("event origin: ", event.origin), console.log("event data: ", event.data); - var deferred = $q.defer(); - switch (event.data) { - case "PAID": - console.log("TransactionService PAID"), deferred.resolve("PAID"), ReplaceModel.getState() === NOT_PURCHASED ? Service.getPurchases() : ViewStateService.viewRequested("downloads"), AnalyticsService.sendData(null, "transaction"), Service.getUserInfo(); - break; - case "CANCELED": - deferred.reject("CANCELED"), console.log("TransactionService CANCELED"); - break; - case "PENDING": - console.log("TransactionService PENDING"), deferred.reject("PENDING"); - break; - default: - deferred.reject("UNKNOWN") - } - return deferred.promise - } -}]), app.service("UnzipService", ["$rootScope", "DeleteOnFileSystemService", "ReplaceModel", "ImportAEService", function($rootScope, DeleteOnFileSystemService, ReplaceModel, ImportAEService) { - var call = { - unzippedCounter: 0, - deletedCounter: 0, - numOfItems: 0, - items: [], - deleteObjects: [], - itemObjects: [], - unzipItems: function(items) { - call.unzippedCounter = 0, call.deletedCounter = 0, call.numOfItems = items.length, call.items = items, call.deleteObjects = [], call.itemObjects = [], call.items.forEach(function(item) { - var itemObj = { - dest: item.downloadDestination + "AE " + item.id, - source: item.downloadDestination + item.fileName - }; - call.itemObjects.push(itemObj), call.deleteObjects.push(itemObj.source, itemObj.dest + path.sep + "__MACOSX"), call.unzip(itemObj) - }), console.log("UnzipService unzipItems numOfItems:", call.numOfItems), console.log("UnzipService unzipItems call.deleteObjects:", call.deleteObjects), console.log("UnzipService unzipItems call.deleteObjects.length:", call.deleteObjects.length) - }, - unzip: function(itemObj) { - var unzipper = new DecompressZip(itemObj.source); - unzipper.on("error", function(err) { - console.log("UnzipService Caught an error: ", err) - }), unzipper.on("extract", function(log) { - console.log("UnzipService Finished extracting"), call.unzippedCounter++, call.unzippedCounter === call.numOfItems && (console.log("UnzipService Finished extracting all items, unzippedCounter", call.unzippedCounter), DeleteOnFileSystemService.deleteFolder(call.deleteObjects, function() { - console.log("UnzipService zip or mac os folder deleted"), call.deletedCounter++, console.log("UnzipService call.deletedCounter: ", call.deletedCounter), console.log("UnzipService call.deleteObjects.length: ", call.deleteObjects.length), call.deletedCounter === call.deleteObjects.length && (console.log("UnzipService ALL zip or mac os folders deleted", ReplaceModel.getState()), call.itemObjects.forEach(function(item) { - ReplaceModel.getState() === NOT_DOWNLOADED && "AEFT" == HOST_NAME && ImportAEService.import(item.dest) - }), ReplaceModel.getState() === DEFAULT && 1 < call.numOfItems ? opn(call.items[0].downloadDestination) : ReplaceModel.getState() === DEFAULT && 1 === call.numOfItems && (console.log("UnzipService opn finder"), opn(itemObj.dest)), ReplaceModel.setState(DEFAULT)) - })) - }), unzipper.on("progress", function(fileIndex, fileCount) { - console.log("UnzipService Extracted file " + (fileIndex + 1) + " of " + fileCount) - }), unzipper.extract({ - path: itemObj.dest - }) - } - }; - return call -}]), app.factory("UserService", ["$rootScope", "AppModel", "LoginModel", function($rootScope, AppModel, LoginModel) { - var file, parsedLocalXML, cm, cx, result = { - readXML: function() { - file = AppModel.getUserXML(), fs.readFile(file, "utf8", function(err, data) { - if (err) throw err; - result.parseLocalXML(data) - }) - }, - saveData: function(cx, cm) { - parsedLocalXML.root.user[0].$.cm = cm, parsedLocalXML.root.user[0].$.cx = cx, result.writeToDisk() - }, - parseLocalXML: function(xml) { - var parser = new xml2js.Parser; - parser.addListener("end", function(res) { - if (cm = (parsedLocalXML = res).root.user[0].$.cm, cx = res.root.user[0].$.cx, 0 < cm.length && 0 < cx.length) { - LoginModel.setCX(cx), LoginModel.setCM(cm); - $rootScope.$emit("api call", { - fn: "getUserInfo" - }) - } - }), parser.parseString(xml) - }, - writeToDisk: function() { - var xml = (new xml2js.Builder).buildObject(parsedLocalXML); - fs.writeFile(file, xml, function(err) { - if (err) throw err - }) - } - }; - return result -}]), app.factory("ViewStateService", ["$rootScope", "ViewStateModel", "ReplaceModel", "LoginModel", function($rootScope, ViewStateModel, ReplaceModel, LoginModel) { - var requestedState, result = { - viewRequested: function(state) { - console.log("ViewStateService viewRequested: ", state), "downloads" !== (requestedState = state) && "previews" !== requestedState && "cart" !== requestedState || LoginModel.getLoggedIn() ? (ViewStateModel.setState(state), result.onViewApproved(!0)) : $rootScope.$emit("modal not logged in", [ERROR]) - }, - onViewApproved: function(result) { - if (console.log("ViewStateService onViewApproved ", result, requestedState), result) { - var fName; - switch (ViewStateModel.setState(requestedState), requestedState) { - case "downloads": - fName = "getPurchases"; - break; - case "previews": - fName = "getImportedPreviews"; - break; - case "cart": - fName = "getCart"; - break; - case "freebies": - fName = "getFreeClips"; - break; - case "bins": - fName = "getBin"; - break; - case "search": - default: - fName = "search" - } - $rootScope.$emit("api call", { - fn: fName - }) - } else console.log("ViewStateService onViewApproved cancel clicked in modal, stay in current view") - } - }; - return result -}]); -var imgHeight, imgWidth, COUNTRIES = [{ - name: "United States", - code: "US" - }, { - name: "Afghanistan", - code: "AF" - }, { - name: "Aland Islands", - code: "AX" - }, { - name: "Albania", - code: "AL" - }, { - name: "Algeria", - code: "DZ" - }, { - name: "American Samoa", - code: "AS" - }, { - name: "Andorra", - code: "AD" - }, { - name: "Angola", - code: "AO" - }, { - name: "Anguilla", - code: "AI" - }, { - name: "Antarctica", - code: "AQ" - }, { - name: "Antigua and Barbuda", - code: "AG" - }, { - name: "Argentina", - code: "AR" - }, { - name: "Armenia", - code: "AM" - }, { - name: "Aruba", - code: "AW" - }, { - name: "Australia", - code: "AU" - }, { - name: "Austria", - code: "AT" - }, { - name: "Azerbaijan", - code: "AZ" - }, { - name: "Bahamas", - code: "BS" - }, { - name: "Bahrain", - code: "BH" - }, { - name: "Bangladesh", - code: "BD" - }, { - name: "Barbados", - code: "BB" - }, { - name: "Belarus", - code: "BY" - }, { - name: "Belgium", - code: "BE" - }, { - name: "Belize", - code: "BZ" - }, { - name: "Benin", - code: "BJ" - }, { - name: "Bermuda", - code: "BM" - }, { - name: "Bhutan", - code: "BT" - }, { - name: "Bolivia", - code: "BO" - }, { - name: "Bosnia and Herzegovina", - code: "BA" - }, { - name: "Botswana", - code: "BW" - }, { - name: "Bouvet Island", - code: "BV" - }, { - name: "Brazil", - code: "BR" - }, { - name: "British Indian Ocean Territory", - code: "IO" - }, { - name: "Brunei Darussalam", - code: "BN" - }, { - name: "Bulgaria", - code: "BG" - }, { - name: "Burkina Faso", - code: "BF" - }, { - name: "Burundi", - code: "BI" - }, { - name: "Cambodia", - code: "KH" - }, { - name: "Cameroon", - code: "CM" - }, { - name: "Canada", - code: "CA" - }, { - name: "Cape Verde", - code: "CV" - }, { - name: "Cayman Islands", - code: "KY" - }, { - name: "Central African Republic", - code: "CF" - }, { - name: "Chad", - code: "TD" - }, { - name: "Chile", - code: "CL" - }, { - name: "China", - code: "CN" - }, { - name: "Christmas Island", - code: "CX" - }, { - name: "Cocos (Keeling) Islands", - code: "CC" - }, { - name: "Colombia", - code: "CO" - }, { - name: "Comoros", - code: "KM" - }, { - name: "Congo", - code: "CG" - }, { - name: "Congo, The Democratic Republic of the", - code: "CD" - }, { - name: "Cook Islands", - code: "CK" - }, { - name: "Costa Rica", - code: "CR" - }, { - name: "Cote D'Ivoire", - code: "CI" - }, { - name: "Croatia", - code: "HR" - }, { - name: "Cuba", - code: "CU" - }, { - name: "Cyprus", - code: "CY" - }, { - name: "Czech Republic", - code: "CZ" - }, { - name: "Denmark", - code: "DK" - }, { - name: "Djibouti", - code: "DJ" - }, { - name: "Dominica", - code: "DM" - }, { - name: "Dominican Republic", - code: "DO" - }, { - name: "Ecuador", - code: "EC" - }, { - name: "Egypt", - code: "EG" - }, { - name: "El Salvador", - code: "SV" - }, { - name: "Equatorial Guinea", - code: "GQ" - }, { - name: "Eritrea", - code: "ER" - }, { - name: "Estonia", - code: "EE" - }, { - name: "Ethiopia", - code: "ET" - }, { - name: "Falkland Islands (Malvinas)", - code: "FK" - }, { - name: "Faroe Islands", - code: "FO" - }, { - name: "Fiji", - code: "FJ" - }, { - name: "Finland", - code: "FI" - }, { - name: "France", - code: "FR" - }, { - name: "French Guiana", - code: "GF" - }, { - name: "French Polynesia", - code: "PF" - }, { - name: "French Southern Territories", - code: "TF" - }, { - name: "Gabon", - code: "GA" - }, { - name: "Gambia", - code: "GM" - }, { - name: "Georgia", - code: "GE" - }, { - name: "Germany", - code: "DE" - }, { - name: "Ghana", - code: "GH" - }, { - name: "Gibraltar", - code: "GI" - }, { - name: "Greece", - code: "GR" - }, { - name: "Greenland", - code: "GL" - }, { - name: "Grenada", - code: "GD" - }, { - name: "Guadeloupe", - code: "GP" - }, { - name: "Guam", - code: "GU" - }, { - name: "Guatemala", - code: "GT" - }, { - name: "Guernsey", - code: "GG" - }, { - name: "Guinea", - code: "GN" - }, { - name: "Guinea-Bissau", - code: "GW" - }, { - name: "Guyana", - code: "GY" - }, { - name: "Haiti", - code: "HT" - }, { - name: "Heard Island and Mcdonald Islands", - code: "HM" - }, { - name: "Holy See (Vatican City State)", - code: "VA" - }, { - name: "Honduras", - code: "HN" - }, { - name: "Hong Kong", - code: "HK" - }, { - name: "Hungary", - code: "HU" - }, { - name: "Iceland", - code: "IS" - }, { - name: "India", - code: "IN" - }, { - name: "Indonesia", - code: "ID" - }, { - name: "Iran, Islamic Republic Of", - code: "IR" - }, { - name: "Iraq", - code: "IQ" - }, { - name: "Ireland", - code: "IE" - }, { - name: "Isle of Man", - code: "IM" - }, { - name: "Israel", - code: "IL" - }, { - name: "Italy", - code: "IT" - }, { - name: "Jamaica", - code: "JM" - }, { - name: "Japan", - code: "JP" - }, { - name: "Jersey", - code: "JE" - }, { - name: "Jordan", - code: "JO" - }, { - name: "Kazakhstan", - code: "KZ" - }, { - name: "Kenya", - code: "KE" - }, { - name: "Kiribati", - code: "KI" - }, { - name: "Korea, Democratic People's Republic of", - code: "KP" - }, { - name: "Korea, Republic of", - code: "KR" - }, { - name: "Kuwait", - code: "KW" - }, { - name: "Kyrgyzstan", - code: "KG" - }, { - name: "Lao People's Democratic Republic", - code: "LA" - }, { - name: "Latvia", - code: "LV" - }, { - name: "Lebanon", - code: "LB" - }, { - name: "Lesotho", - code: "LS" - }, { - name: "Liberia", - code: "LR" - }, { - name: "Libyan Arab Jamahiriya", - code: "LY" - }, { - name: "Liechtenstein", - code: "LI" - }, { - name: "Lithuania", - code: "LT" - }, { - name: "Luxembourg", - code: "LU" - }, { - name: "Macao", - code: "MO" - }, { - name: "Macedonia, The Former Yugoslav Republic of", - code: "MK" - }, { - name: "Madagascar", - code: "MG" - }, { - name: "Malawi", - code: "MW" - }, { - name: "Malaysia", - code: "MY" - }, { - name: "Maldives", - code: "MV" - }, { - name: "Mali", - code: "ML" - }, { - name: "Malta", - code: "MT" - }, { - name: "Marshall Islands", - code: "MH" - }, { - name: "Martinique", - code: "MQ" - }, { - name: "Mauritania", - code: "MR" - }, { - name: "Mauritius", - code: "MU" - }, { - name: "Mayotte", - code: "YT" - }, { - name: "Mexico", - code: "MX" - }, { - name: "Micronesia, Federated States of", - code: "FM" - }, { - name: "Moldova, Republic of", - code: "MD" - }, { - name: "Monaco", - code: "MC" - }, { - name: "Mongolia", - code: "MN" - }, { - name: "Montserrat", - code: "MS" - }, { - name: "Morocco", - code: "MA" - }, { - name: "Mozambique", - code: "MZ" - }, { - name: "Myanmar", - code: "MM" - }, { - name: "Namibia", - code: "NA" - }, { - name: "Nauru", - code: "NR" - }, { - name: "Nepal", - code: "NP" - }, { - name: "Netherlands", - code: "NL" - }, { - name: "Netherlands Antilles", - code: "AN" - }, { - name: "New Caledonia", - code: "NC" - }, { - name: "New Zealand", - code: "NZ" - }, { - name: "Nicaragua", - code: "NI" - }, { - name: "Niger", - code: "NE" - }, { - name: "Nigeria", - code: "NG" - }, { - name: "Niue", - code: "NU" - }, { - name: "Norfolk Island", - code: "NF" - }, { - name: "Northern Mariana Islands", - code: "MP" - }, { - name: "Norway", - code: "NO" - }, { - name: "Oman", - code: "OM" - }, { - name: "Pakistan", - code: "PK" - }, { - name: "Palau", - code: "PW" - }, { - name: "Palestinian Territory, Occupied", - code: "PS" - }, { - name: "Panama", - code: "PA" - }, { - name: "Papua New Guinea", - code: "PG" - }, { - name: "Paraguay", - code: "PY" - }, { - name: "Peru", - code: "PE" - }, { - name: "Philippines", - code: "PH" - }, { - name: "Pitcairn", - code: "PN" - }, { - name: "Poland", - code: "PL" - }, { - name: "Portugal", - code: "PT" - }, { - name: "Puerto Rico", - code: "PR" - }, { - name: "Qatar", - code: "QA" - }, { - name: "Reunion", - code: "RE" - }, { - name: "Romania", - code: "RO" - }, { - name: "Russian Federation", - code: "RU" - }, { - name: "Rwanda", - code: "RW" - }, { - name: "Saint Helena", - code: "SH" - }, { - name: "Saint Kitts and Nevis", - code: "KN" - }, { - name: "Saint Lucia", - code: "LC" - }, { - name: "Saint Pierre and Miquelon", - code: "PM" - }, { - name: "Saint Vincent and the Grenadines", - code: "VC" - }, { - name: "Samoa", - code: "WS" - }, { - name: "San Marino", - code: "SM" - }, { - name: "Sao Tome and Principe", - code: "ST" - }, { - name: "Saudi Arabia", - code: "SA" - }, { - name: "Senegal", - code: "SN" - }, { - name: "Serbia and Montenegro", - code: "CS" - }, { - name: "Seychelles", - code: "SC" - }, { - name: "Sierra Leone", - code: "SL" - }, { - name: "Singapore", - code: "SG" - }, { - name: "Slovakia", - code: "SK" - }, { - name: "Slovenia", - code: "SI" - }, { - name: "Solomon Islands", - code: "SB" - }, { - name: "Somalia", - code: "SO" - }, { - name: "South Africa", - code: "ZA" - }, { - name: "South Georgia and the South Sandwich Islands", - code: "GS" - }, { - name: "Spain", - code: "ES" - }, { - name: "Sri Lanka", - code: "LK" - }, { - name: "Sudan", - code: "SD" - }, { - name: "Suriname", - code: "SR" - }, { - name: "Svalbard and Jan Mayen", - code: "SJ" - }, { - name: "Swaziland", - code: "SZ" - }, { - name: "Sweden", - code: "SE" - }, { - name: "Switzerland", - code: "CH" - }, { - name: "Syrian Arab Republic", - code: "SY" - }, { - name: "Taiwan, Province of China", - code: "TW" - }, { - name: "Tajikistan", - code: "TJ" - }, { - name: "Tanzania, United Republic of", - code: "TZ" - }, { - name: "Thailand", - code: "TH" - }, { - name: "Timor-Leste", - code: "TL" - }, { - name: "Togo", - code: "TG" - }, { - name: "Tokelau", - code: "TK" - }, { - name: "Tonga", - code: "TO" - }, { - name: "Trinidad and Tobago", - code: "TT" - }, { - name: "Tunisia", - code: "TN" - }, { - name: "Turkey", - code: "TR" - }, { - name: "Turkmenistan", - code: "TM" - }, { - name: "Turks and Caicos Islands", - code: "TC" - }, { - name: "Tuvalu", - code: "TV" - }, { - name: "Uganda", - code: "UG" - }, { - name: "Ukraine", - code: "UA" - }, { - name: "United Arab Emirates", - code: "AE" - }, { - name: "United Kingdom", - code: "GB" - }, { - name: "United States", - code: "US" - }, { - name: "United States Minor Outlying Islands", - code: "UM" - }, { - name: "Uruguay", - code: "UY" - }, { - name: "Uzbekistan", - code: "UZ" - }, { - name: "Vanuatu", - code: "VU" - }, { - name: "Venezuela", - code: "VE" - }, { - name: "Vietnam", - code: "VN" - }, { - name: "Virgin Islands, British", - code: "VG" - }, { - name: "Virgin Islands, U.S.", - code: "VI" - }, { - name: "Wallis and Futuna", - code: "WF" - }, { - name: "Western Sahara", - code: "EH" - }, { - name: "Yemen", - code: "YE" - }, { - name: "Zambia", - code: "ZM" - }, { - name: "Zimbabwe", - code: "ZW" - }], - STATES = [{ - name: "Alabama", - label: "Alabama", - code: "AL" - }, { - name: "Alaska", - label: "Alaska", - code: "AK" - }, { - name: "American Samoa", - label: "American Samoa", - code: "AS" - }, { - name: "Arizona", - label: "Arizona", - code: "AZ" - }, { - name: "Arkansas", - label: "Arkansas", - code: "AR" - }, { - name: "Armed Forces Europe", - label: "Armed Forces Europe", - code: "AE" - }, { - name: "Armed Forces Pacific", - label: "Armed Forces Pacific", - code: "AP" - }, { - name: "Armed Forces the Americas", - label: "Armed Forces the Americas", - code: "AA" - }, { - name: "California", - label: "California", - code: "CA" - }, { - name: "Colorado", - label: "Colorado", - code: "CO" - }, { - name: "Connecticut", - label: "Connecticut", - code: "CT" - }, { - name: "Delaware", - label: "Delaware", - code: "DE" - }, { - name: "District of Columbia", - label: "District of Columbia", - code: "DC" - }, { - name: "Federated States of Micronesia", - label: "Federated States of Micronesia", - code: "FM" - }, { - name: "Florida", - label: "Florida", - code: "FL" - }, { - name: "Georgia", - label: "Georgia", - code: "GA" - }, { - name: "Guam", - label: "Guam", - code: "GU" - }, { - name: "Hawaii", - label: "Hawaii", - code: "HI" - }, { - name: "Idaho", - label: "Idaho", - code: "ID" - }, { - name: "Illinois", - label: "Illinois", - code: "IL" - }, { - name: "Indiana", - label: "Indiana", - code: "IN" - }, { - name: "Iowa", - label: "Iowa", - code: "IA" - }, { - name: "Kansas", - label: "Kansas", - code: "KS" - }, { - name: "Kentucky", - label: "Kentucky", - code: "KY" - }, { - name: "Louisiana", - label: "Louisiana", - code: "LA" - }, { - name: "Maine", - label: "Maine", - code: "ME" - }, { - name: "Marshall Islands", - label: "Marshall Islands", - code: "MH" - }, { - name: "Maryland", - label: "Maryland", - code: "MD" - }, { - name: "Massachusetts", - label: "Massachusetts", - code: "MA" - }, { - name: "Michigan", - label: "Michigan", - code: "MI" - }, { - name: "Minnesota", - label: "Minnesota", - code: "MN" - }, { - name: "Mississippi", - label: "Mississippi", - code: "MS" - }, { - name: "Missouri", - label: "Missouri", - code: "MO" - }, { - name: "Montana", - label: "Montana", - code: "MT" - }, { - name: "Nebraska", - label: "Nebraska", - code: "NE" - }, { - name: "Nevada", - label: "Nevada", - code: "NV" - }, { - name: "New Hampshire", - label: "New Hampshire", - code: "NH" - }, { - name: "New Jersey", - label: "New Jersey", - code: "NJ" - }, { - name: "New Mexico", - label: "New Mexico", - code: "NM" - }, { - name: "New York", - label: "New York", - code: "NY" - }, { - name: "North Carolina", - label: "North Carolina", - code: "NC" - }, { - name: "North Dakota", - label: "North Dakota", - code: "ND" - }, { - name: "Northern Mariana Islands", - label: "Northern Mariana Islands", - code: "MP" - }, { - name: "Ohio", - label: "Ohio", - code: "OH" - }, { - name: "Oklahoma", - label: "Oklahoma", - code: "OK" - }, { - name: "Oregon", - label: "Oregon", - code: "OR" - }, { - name: "Pennsylvania", - label: "Pennsylvania", - code: "PA" - }, { - name: "Puerto Rico", - label: "Puerto Rico", - code: "PR" - }, { - name: "Rhode Island", - label: "Rhode Island", - code: "RI" - }, { - name: "South Carolina", - label: "South Carolina", - code: "SC" - }, { - name: "South Dakota", - label: "South Dakota", - code: "SD" - }, { - name: "Tennessee", - label: "Tennessee", - code: "TN" - }, { - name: "Texas", - label: "Texas", - code: "TX" - }, { - name: "Utah", - label: "Utah", - code: "UT" - }, { - name: "Vermont", - label: "Vermont", - code: "VT" - }, { - name: "Virgin Islands, U.S.", - label: "Virgin Islands, U.S.", - code: "VI" - }, { - name: "Virginia", - label: "Virginia", - code: "VA" - }, { - name: "Washington", - label: "Washington", - code: "WA" - }, { - name: "West Virginia", - label: "West Virginia", - code: "WV" - }, { - name: "Wisconsin", - label: "Wisconsin", - code: "WI" - }, { - name: "Wyoming", - label: "Wyoming", - code: "WY" - }]; - -function get_browser() { - var tem, ua = navigator.userAgent, - M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - return /trident/i.test(M[1]) ? "IE " + ((tem = /\brv[ :]+(\d+)/g.exec(ua) || [])[1] || "") : "Chrome" === M[1] && null != (tem = ua.match(/\bOPR\/(\d+)/)) ? "Opera " + tem[1] : (M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"], null != (tem = ua.match(/version\/(\d+)/i)) && M.splice(1, 1, tem[1]), M[0]) -} - -function get_browser_version() { - var tem, ua = navigator.userAgent, - M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - return /trident/i.test(M[1]) ? "IE " + ((tem = /\brv[ :]+(\d+)/g.exec(ua) || [])[1] || "") : "Chrome" === M[1] && null != (tem = ua.match(/\bOPR\/(\d+)/)) ? "Opera " + tem[1] : (M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"], null != (tem = ua.match(/version\/(\d+)/i)) && M.splice(1, 1, tem[1]), M[1]) -} - -function findHHandWW() { - return imgHeight = this.height, imgWidth = this.width, !0 -} - -function showImage(imgPath) { - var myImage = new Image; - myImage.name = imgPath, myImage.onload = findHHandWW, myImage.src = imgPath -} - -function log(className, prefix, obj) { - if (prefix = " " + prefix + ": ", obj instanceof Array) obj.forEach(function(entry) { - log(className, "item", entry) - }); - else - for (key in console.log(className + ":"), obj) console.log(prefix + key + ": " + obj[key]), "formats" === key && obj[key].forEach(function(entry) { - log(className, " format", entry) - }), "versions" === key && obj[key].forEach(function(entry) { - log(className, " versions", entry) - }) -} - -function ExtendedID() {} - -function getAbbrName(name, len) { - return name && name.length > len ? name.slice(0, len) + "..." : name -} - -function convertArrayToCommaSeperatedString(ids) { - var idsToString = ""; - return ids.forEach(function(id) { - idsToString += id + "," - }), idsToString = idsToString.slice(0, -1) -} - -function getFormattedName(input) { - for (; - 1 != input.indexOf(",");) input = input.replace(",", " "); - for (; - 1 != input.indexOf("&");) input = input.replace("&", "and"); - for (; - 1 != input.indexOf("/");) input = input.replace("/", " "); - for (; - 1 != input.indexOf("'");) input = input.replace("'", " "); - for (; - 1 != input.indexOf("(");) input = input.replace("(", " "); - for (; - 1 != input.indexOf(")");) input = input.replace(")", " "); - for (; - 1 != input.indexOf(":");) input = input.replace(":", " "); - for (; - 1 != input.indexOf(" ");) input = input.replace(" ", " "); - return input -} - -function getUID() { - var d = (new Date).getTime(); - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { - var r = (d + 16 * Math.random()) % 16 | 0; - return d = Math.floor(d / 16), ("x" == c ? r : 3 & r | 8).toString(16) - }) -} - -function getStringPosition(string, subString, index) { - return string.split(subString, index).join(subString).length -} - -function gup(name, url) { - url || (url = location.href), name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - var results = new RegExp("[\\?&]" + name + "=([^&#]*)").exec(url); - return null == results ? null : results[1] -} - -function checkVersion(tv, uv) { - var updaterVersion = uv; - if (tv === updaterVersion) return !1; - var splitThis = tv.split("."), - splitThisInt = []; - splitThis.forEach(function(string) { - splitThisInt.push(parseInt(string)) - }); - var splitUpdater = updaterVersion.split("."), - splitUpdaterInt = []; - return splitUpdater.forEach(function(string) { - splitUpdaterInt.push(parseInt(string)) - }), splitUpdaterInt[0] > splitThisInt[0] || (splitUpdaterInt[0] >= splitThisInt[0] && splitUpdaterInt[1] > splitThisInt[1] || splitUpdaterInt[0] >= splitThisInt[0] && splitUpdaterInt[1] >= splitThisInt[1] && splitUpdaterInt[2] > splitThisInt[2]) -} - -function getConvertedVideoStandard(vs) { - var standard; - switch (parseInt(vs)) { - case 0: - standard = "Multimedia / Unknown"; - break; - case 1: - standard = "NTSC D1"; - break; - case 2: - standard = "NTSC DV"; - break; - case 3: - standard = "PAL / PAL DV"; - break; - case 4: - standard = "HD 1080"; - break; - case 5: - standard = "HDV 720p"; - break; - case 6: - standard = "Other Hi-Def"; - break; - case 7: - standard = "Multimedia"; - break; - case 8: - standard = "HDV 1080i"; - break; - case 9: - standard = "HD 720"; - break; - case 10: - standard = "4k+"; - break; - case 100: - standard = "Music"; - break; - case 101: - standard = "Sound effect"; - break; - case 200: - standard = "AE"; - break; - case 300: - standard = "Photo"; - break; - case 301: - standard = "Illustration"; - break; - case 400: - standard = "3D" - } - return standard -} - -function getMediaType(vs) { - var type; - switch (vs) { - case "Music": - case "Sound effect": - case "Photo": - case "Illustration": - case "AE": - type = vs; - break; - default: - type = "Video" - } - return type -} -Number.prototype.formatMoney = function(decPlaces, thouSeparator, decSeparator, currencySymbol) { - decPlaces = isNaN(decPlaces = Math.abs(decPlaces)) ? 2 : decPlaces, decSeparator = null == decSeparator ? "." : decSeparator, thouSeparator = null == thouSeparator ? "," : thouSeparator, currencySymbol = null == currencySymbol ? "$" : currencySymbol; - var n = this, - sign = n < 0 ? "-" : "", - i = parseInt(n = Math.abs(+n || 0).toFixed(decPlaces)) + "", - j = 3 < (j = i.length) ? j % 3 : 0; - return sign + currencySymbol + (j ? i.substr(0, j) + thouSeparator : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thouSeparator) + (decPlaces ? decSeparator + Math.abs(n - i).toFixed(decPlaces).slice(2) : "") - }, - function() { - function Point(x, y) { - this.x = x || 0, this.y = y || 0 - } - Point.prototype.x = null, Point.prototype.y = null, Point.prototype.add = function(v) { - return new Point(this.x + v.x, this.y + v.y) - }, Point.prototype.clone = function() { - return new Point(this.x, this.y) - }, Point.prototype.degreesTo = function(v) { - var dx = this.x - v.x, - dy = this.y - v.y; - return Math.atan2(dy, dx) * (180 / Math.PI) - }, Point.prototype.distance = function(v) { - var x = this.x - v.x, - y = this.y - v.y; - return Math.sqrt(x * x + y * y) - }, Point.prototype.equals = function(toCompare) { - return this.x == toCompare.x && this.y == toCompare.y - }, Point.prototype.interpolate = function(v, f) { - return new Point((this.x + v.x) * f, (this.y + v.y) * f) - }, Point.prototype.length = function() { - return Math.sqrt(this.x * this.x + this.y * this.y) - }, Point.prototype.normalize = function(thickness) { - var l = this.length(); - this.x = this.x / l * thickness, this.y = this.y / l * thickness - }, Point.prototype.orbit = function(origin, arcWidth, arcHeight, degrees) { - var radians = degrees * (Math.PI / 180); - this.x = origin.x + arcWidth * Math.cos(radians), this.y = origin.y + arcHeight * Math.sin(radians) - }, Point.prototype.offset = function(dx, dy) { - this.x += dx, this.y += dy - }, Point.prototype.subtract = function(v) { - return new Point(this.x - v.x, this.y - v.y) - }, Point.prototype.toString = function() { - return "(x=" + this.x + ", y=" + this.y + ")" - }, Point.interpolate = function(pt1, pt2, f) { - return new Point((pt1.x + pt2.x) * f, (pt1.y + pt2.y) * f) - }, Point.polar = function(len, angle) { - return new Point(len * Math.sin(angle), len * Math.cos(angle)) - }, Point.distance = function(pt1, pt2) { - var x = pt1.x - pt2.x, - y = pt1.y - pt2.y; - return Math.sqrt(x * x + y * y) - }, this.Point = window.Point = Point - }(), ExtendedID.extend = function(id) { - if (id) { - for (var extendedID = id.toString(); extendedID.length < 9;) extendedID = "0" + extendedID; - return extendedID - } - }, String.prototype.insert = function(index, string) { - return 0 < index ? this.substring(0, index) + string + this.substring(index, this.length) : string + this - }, String.prototype.replaceAll = function(search, replacement) { - return this.replace(new RegExp(search, "g"), replacement) - }, getMousePosition = function(element) { - for (var xPosition = 0, yPosition = 0; element;) xPosition += element.offsetLeft - element.scrollLeft + element.clientLeft, yPosition += element.offsetTop - element.scrollTop + element.clientTop, element = element.offsetParent; - return { - x: xPosition, - y: yPosition - } - }, getScroll = function() { - if (null != window.pageYOffset) return [pageXOffset, pageYOffset]; - var d = document, - r = d.documentElement, - b = d.body; - return [r.scrollLeft || b.scrollLeft || 0, r.scrollTop || b.scrollTop || 0] - }, getUserHome = function() { - return require("os").homedir() - }, getName = function(input) { - for (; - 1 != input.indexOf(",");) input = input.replace(",", " "); - for (; - 1 != input.indexOf("&");) input = input.replace("&", "and"); - for (; - 1 != input.indexOf("/");) input = input.replace("/", " "); - for (; - 1 != input.indexOf("'");) input = input.replace("'", " "); - for (; - 1 != input.indexOf("(");) input = input.replace("(", " "); - for (; - 1 != input.indexOf(")");) input = input.replace(")", " "); - for (; - 1 != input.indexOf(":");) input = input.replace(":", " "); - return input - }, getPosition = function(element) { - for (var xPosition = 0, yPosition = 0; element;) xPosition += element.offsetLeft - element.scrollLeft + element.clientLeft, yPosition += element.offsetTop - element.scrollTop + element.clientTop, element = element.offsetParent; - return { - x: xPosition, - y: yPosition - } - }, getChromeVersion = function() { - var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - return !!raw && parseInt(raw[2], 10) - }; diff --git a/pype/premiere/extensions/com.pype.avalon/ppro/js/vendor/bootstrap.min.js b/pype/premiere/extensions/com.pype.avalon/ppro/js/vendor/bootstrap.min.js deleted file mode 100644 index 9df6b6c2ce..0000000000 --- a/pype/premiere/extensions/com.pype.avalon/ppro/js/vendor/bootstrap.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v4.2.1 (https://getbootstrap.com/) - * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("popper.js"),require("jquery")):"function"==typeof define&&define.amd?define(["exports","popper.js","jquery"],e):e(t.bootstrap={},t.Popper,t.jQuery)}(this,function(t,u,g){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
    ',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},De="show",we="out",Ae={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:"show"+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Ne="fade",Oe="show",ke=".tooltip-inner",Pe=".arrow",Le="hover",je="focus",He="click",Re="manual",Ue=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Oe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(Ne);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:Pe},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Oe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(g(this.tip).hasClass(Ne)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Oe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[He]=!1,this._activeTrigger[je]=!1,this._activeTrigger[Le]=!1,g(this.tip).hasClass(Ne)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ce+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(ke)),this.getTitle()),g(t).removeClass(Ne+" "+Oe)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return be[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Re){var e=t===Le?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Le?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?je:Le]=!0),g(e.getTipElement()).hasClass(Oe)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?je:Le]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,g(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(pe,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Te);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(Ne),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(ve),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(ve,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.2.1"}},{key:"Default",get:function(){return Ie}},{key:"NAME",get:function(){return pe}},{key:"DATA_KEY",get:function(){return ve}},{key:"Event",get:function(){return Ae}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}();g.fn[pe]=Ue._jQueryInterface,g.fn[pe].Constructor=Ue,g.fn[pe].noConflict=function(){return g.fn[pe]=ye,Ue._jQueryInterface};var We="popover",xe="bs.popover",Fe="."+xe,qe=g.fn[We],Me="bs-popover",Ke=new RegExp("(^|\\s)"+Me+"\\S+","g"),Qe=l({},Ue.Default,{placement:"right",trigger:"click",content:"",template:''}),Be=l({},Ue.DefaultType,{content:"(string|element|function)"}),Ve="fade",Ye="show",Xe=".popover-header",ze=".popover-body",Ge={HIDE:"hide"+Fe,HIDDEN:"hidden"+Fe,SHOW:"show"+Fe,SHOWN:"shown"+Fe,INSERTED:"inserted"+Fe,CLICK:"click"+Fe,FOCUSIN:"focusin"+Fe,FOCUSOUT:"focusout"+Fe,MOUSEENTER:"mouseenter"+Fe,MOUSELEAVE:"mouseleave"+Fe},Je=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Me+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(Xe),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ze),e),t.removeClass(Ve+" "+Ye)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ke);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n}\n\nfunction setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst Util = {\n\n TRANSITION_END: 'bsTransitionEnd',\n\n getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID) // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix))\n return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n\n if (!selector || selector === '#') {\n const hrefAttr = element.getAttribute('href')\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''\n }\n\n return selector && document.querySelector(selector) ? selector : null\n },\n\n getTransitionDurationFromElement(element) {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let transitionDuration = $(element).css('transition-duration')\n let transitionDelay = $(element).css('transition-delay')\n\n const floatTransitionDuration = parseFloat(transitionDuration)\n const floatTransitionDelay = parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END)\n },\n\n // TODO: Remove in v5\n supportsTransitionEnd() {\n return Boolean(TRANSITION_END)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n },\n\n findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return Util.findShadowRoot(element.parentNode)\n }\n}\n\nsetTransitionEndSupport()\n\nexport default Util\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Selector = {\n DISMISS : '[data-dismiss=\"alert\"]'\n}\n\nconst Event = {\n CLOSE : `close${EVENT_KEY}`,\n CLOSED : `closed${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n ALERT : 'alert',\n FADE : 'fade',\n SHOW : 'show'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n let rootElement = this._element\n if (element) {\n rootElement = this._getRootElement(element)\n }\n\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n if (!parent) {\n parent = $(element).closest(`.${ClassName.ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(Event.CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(ClassName.SHOW)\n\n if (!$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(element)\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(transitionDuration)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(Event.CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n $element.data(DATA_KEY, data)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(\n Event.CLICK_DATA_API,\n Selector.DISMISS,\n Alert._handleDismiss(new Alert())\n)\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Alert._jQueryInterface\n$.fn[NAME].Constructor = Alert\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n}\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst ClassName = {\n ACTIVE : 'active',\n BUTTON : 'btn',\n FOCUS : 'focus'\n}\n\nconst Selector = {\n DATA_TOGGLE_CARROT : '[data-toggle^=\"button\"]',\n DATA_TOGGLE : '[data-toggle=\"buttons\"]',\n INPUT : 'input:not([type=\"hidden\"])',\n ACTIVE : '.active',\n BUTTON : '.btn'\n}\n\nconst Event = {\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n Selector.DATA_TOGGLE\n )[0]\n\n if (rootElement) {\n const input = this._element.querySelector(Selector.INPUT)\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = rootElement.querySelector(Selector.ACTIVE)\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName.ACTIVE)\n }\n }\n }\n\n if (triggerChangeEvent) {\n if (input.hasAttribute('disabled') ||\n rootElement.hasAttribute('disabled') ||\n input.classList.contains('disabled') ||\n rootElement.classList.contains('disabled')) {\n return\n }\n input.checked = !this._element.classList.contains(ClassName.ACTIVE)\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !this._element.classList.contains(ClassName.ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName.ACTIVE)\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n event.preventDefault()\n\n let button = event.target\n\n if (!$(button).hasClass(ClassName.BUTTON)) {\n button = $(button).closest(Selector.BUTTON)\n }\n\n Button._jQueryInterface.call($(button), 'toggle')\n })\n .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(Selector.BUTTON)[0]\n $(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Button._jQueryInterface\n$.fn[NAME].Constructor = Button\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n}\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\nconst ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true,\n touch : true\n}\n\nconst DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean',\n touch : 'boolean'\n}\n\nconst Direction = {\n NEXT : 'next',\n PREV : 'prev',\n LEFT : 'left',\n RIGHT : 'right'\n}\n\nconst Event = {\n SLIDE : `slide${EVENT_KEY}`,\n SLID : `slid${EVENT_KEY}`,\n KEYDOWN : `keydown${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`,\n TOUCHSTART : `touchstart${EVENT_KEY}`,\n TOUCHMOVE : `touchmove${EVENT_KEY}`,\n TOUCHEND : `touchend${EVENT_KEY}`,\n POINTERDOWN : `pointerdown${EVENT_KEY}`,\n POINTERUP : `pointerup${EVENT_KEY}`,\n DRAG_START : `dragstart${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n CAROUSEL : 'carousel',\n ACTIVE : 'active',\n SLIDE : 'slide',\n RIGHT : 'carousel-item-right',\n LEFT : 'carousel-item-left',\n NEXT : 'carousel-item-next',\n PREV : 'carousel-item-prev',\n ITEM : 'carousel-item',\n POINTER_EVENT : 'pointer-event'\n}\n\nconst Selector = {\n ACTIVE : '.active',\n ACTIVE_ITEM : '.active.carousel-item',\n ITEM : '.carousel-item',\n ITEM_IMG : '.carousel-item img',\n NEXT_PREV : '.carousel-item-next, .carousel-item-prev',\n INDICATORS : '.carousel-indicators',\n DATA_SLIDE : '[data-slide], [data-slide-to]',\n DATA_RIDE : '[data-ride=\"carousel\"]'\n}\n\nconst PointerType = {\n TOUCH : 'touch',\n PEN : 'pen'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel {\n constructor(element, config) {\n this._items = null\n this._interval = null\n this._activeElement = null\n this._isPaused = false\n this._isSliding = false\n this.touchTimeout = null\n this.touchStartX = 0\n this.touchDeltaX = 0\n\n this._config = this._getConfig(config)\n this._element = element\n this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (this._element.querySelector(Selector.NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n $(this._element).one(Event.SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? Direction.NEXT\n : Direction.PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\n this._items = null\n this._config = null\n this._element = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n // swipe left\n if (direction > 0) {\n this.prev()\n }\n\n // swipe right\n if (direction < 0) {\n this.next()\n }\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element)\n .on(Event.KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(Event.MOUSEENTER, (event) => this.pause(event))\n .on(Event.MOUSELEAVE, (event) => this.cycle(event))\n }\n\n this._addTouchEventListeners()\n }\n\n _addTouchEventListeners() {\n if (!this._touchSupported) {\n return\n }\n\n const start = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchStartX = event.originalEvent.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.originalEvent.touches[0].clientX\n }\n }\n\n const move = (event) => {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n this.touchDeltaX = 0\n } else {\n this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX\n }\n }\n\n const end = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchDeltaX = event.originalEvent.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())\n if (this._pointerEvent) {\n $(this._element).on(Event.POINTERDOWN, (event) => start(event))\n $(this._element).on(Event.POINTERUP, (event) => end(event))\n\n this._element.classList.add(ClassName.POINTER_EVENT)\n } else {\n $(this._element).on(Event.TOUCHSTART, (event) => start(event))\n $(this._element).on(Event.TOUCHMOVE, (event) => move(event))\n $(this._element).on(Event.TOUCHEND, (event) => end(event))\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault()\n this.prev()\n break\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode\n ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))\n : []\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === Direction.NEXT\n const isPrevDirection = direction === Direction.PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === Direction.PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))\n const slideEvent = $.Event(Event.SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))\n $(indicators)\n .removeClass(ClassName.ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName.ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName.LEFT\n orderClassName = ClassName.NEXT\n eventDirectionName = Direction.LEFT\n } else {\n directionalClassName = ClassName.RIGHT\n orderClassName = ClassName.PREV\n eventDirectionName = Direction.RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.isDefaultPrevented()) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n\n const slidEvent = $.Event(Event.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if ($(this._element).hasClass(ClassName.SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)\n if (nextElementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval\n this._config.interval = nextElementInterval\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(activeElement)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(ClassName.ACTIVE)\n\n $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(transitionDuration)\n } else {\n $(activeElement).removeClass(ClassName.ACTIVE)\n $(nextElement).addClass(ClassName.ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n data[action]()\n } else if (_config.interval) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const slideIndex = this.getAttribute('data-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel._jQueryInterface.call($(target), config)\n\n if (slideIndex) {\n $(target).data(DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)\n\n$(window).on(Event.LOAD_DATA_API, () => {\n const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))\n for (let i = 0, len = carousels.length; i < len; i++) {\n const $carousel = $(carousels[i])\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Carousel._jQueryInterface\n$.fn[NAME].Constructor = Carousel\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n}\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n toggle : true,\n parent : ''\n}\n\nconst DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n}\n\nconst Event = {\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SHOW : 'show',\n COLLAPSE : 'collapse',\n COLLAPSING : 'collapsing',\n COLLAPSED : 'collapsed'\n}\n\nconst Dimension = {\n WIDTH : 'width',\n HEIGHT : 'height'\n}\n\nconst Selector = {\n ACTIVES : '.show, .collapsing',\n DATA_TOGGLE : '[data-toggle=\"collapse\"]'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = [].slice.call(document.querySelectorAll(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n\n const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = Util.getSelectorFromElement(elem)\n const filterElement = [].slice.call(document.querySelectorAll(selector))\n .filter((foundElem) => foundElem === element)\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(ClassName.SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))\n .filter((elem) => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === this._config.parent\n }\n\n return elem.classList.contains(ClassName.COLLAPSE)\n })\n\n if (actives.length === 0) {\n actives = null\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(Event.SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(ClassName.COLLAPSE)\n .addClass(ClassName.COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n $(this._triggerArray)\n .removeClass(ClassName.COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .addClass(ClassName.SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(Event.SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n const startEvent = $.Event(Event.HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(ClassName.COLLAPSING)\n .removeClass(ClassName.COLLAPSE)\n .removeClass(ClassName.SHOW)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n\n if (selector !== null) {\n const $elem = $([].slice.call(document.querySelectorAll(selector)))\n if (!$elem.hasClass(ClassName.SHOW)) {\n $(trigger).addClass(ClassName.COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .trigger(Event.HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(Dimension.WIDTH)\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT\n }\n\n _getParent() {\n let parent\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0]\n }\n } else {\n parent = document.querySelector(this._config.parent)\n }\n\n const selector =\n `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n\n const children = [].slice.call(parent.querySelectorAll(selector))\n $(children).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n const isOpen = $(element).hasClass(ClassName.SHOW)\n\n if (triggerArray.length) {\n $(triggerArray)\n .toggleClass(ClassName.COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? document.querySelector(selector) : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n // preventDefault only for
    elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault()\n }\n\n const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n const selectors = [].slice.call(document.querySelectorAll(selector))\n\n $(selectors).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Collapse._jQueryInterface\n$.fn[NAME].Constructor = Collapse\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n}\n\nexport default Collapse\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\nconst SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\nconst TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\nconst ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\nconst ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\nconst RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,\n KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DISABLED : 'disabled',\n SHOW : 'show',\n DROPUP : 'dropup',\n DROPRIGHT : 'dropright',\n DROPLEFT : 'dropleft',\n MENURIGHT : 'dropdown-menu-right',\n MENULEFT : 'dropdown-menu-left',\n POSITION_STATIC : 'position-static'\n}\n\nconst Selector = {\n DATA_TOGGLE : '[data-toggle=\"dropdown\"]',\n FORM_CHILD : '.dropdown form',\n MENU : '.dropdown-menu',\n NAVBAR_NAV : '.navbar-nav',\n VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n}\n\nconst AttachmentMap = {\n TOP : 'top-start',\n TOPEND : 'top-end',\n BOTTOM : 'bottom-start',\n BOTTOMEND : 'bottom-end',\n RIGHT : 'right-start',\n RIGHTEND : 'right-end',\n LEFT : 'left-start',\n LEFTEND : 'left-end'\n}\n\nconst Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent',\n reference : 'toggle',\n display : 'dynamic'\n}\n\nconst DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)',\n reference : '(string|element)',\n display : 'string'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown {\n constructor(element, config) {\n this._element = element\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this._element)\n const isActive = $(this._menu).hasClass(ClassName.SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = parent\n } else if (Util.isElement(this._config.reference)) {\n referenceElement = this._config.reference\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n }\n\n // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName.POSITION_STATIC)\n }\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n $(parent).closest(Selector.NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n show() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n hide() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(Event.CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n\n if (parent) {\n this._menu = parent.querySelector(Selector.MENU)\n }\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element.parentNode)\n let placement = AttachmentMap.BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(ClassName.DROPUP)) {\n placement = AttachmentMap.TOP\n if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.TOPEND\n }\n } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT\n } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {\n placement = AttachmentMap.LEFT\n } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getPopperConfig() {\n const offsetConf = {}\n if (typeof this._config.offset === 'function') {\n offsetConf.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets) || {}\n }\n return data\n }\n } else {\n offsetConf.offset = this._config.offset\n }\n\n const popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: offsetConf,\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n }\n }\n\n // Disable Popper.js if we have a static display\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n }\n }\n return popperConfig\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!$(parent).hasClass(ClassName.SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n $(dropdownMenu).removeClass(ClassName.SHOW)\n $(parent)\n .removeClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(ClassName.SHOW)\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n const toggle = parent.querySelector(Selector.DATA_TOGGLE)\n $(toggle).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)\n .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Dropdown._jQueryInterface\n$.fn[NAME].Constructor = Dropdown\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n}\n\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\nconst Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n}\n\nconst DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n RESIZE : `resize${EVENT_KEY}`,\n CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,\n KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,\n MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,\n MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SCROLLBAR_MEASURER : 'modal-scrollbar-measure',\n BACKDROP : 'modal-backdrop',\n OPEN : 'modal-open',\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n DIALOG : '.modal-dialog',\n DATA_TOGGLE : '[data-toggle=\"modal\"]',\n DATA_DISMISS : '[data-dismiss=\"modal\"]',\n FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT : '.sticky-top'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = element.querySelector(Selector.DIALOG)\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._isTransitioning = false\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(Event.SHOW, {\n relatedTarget\n })\n\n $(this._element).trigger(showEvent)\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n Event.CLICK_DISMISS,\n Selector.DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {\n $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = $.Event(Event.HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(Event.FOCUSIN)\n\n $(this._element).removeClass(ClassName.SHOW)\n\n $(this._element).off(Event.CLICK_DISMISS)\n $(this._dialog).off(Event.MOUSEDOWN_DISMISS)\n\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))\n\n /**\n * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `Event.CLICK_DATA_API` event that should remain\n */\n $(document).off(Event.FOCUSIN)\n\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._element = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._isTransitioning = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _showElement(relatedTarget) {\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (!this._element.parentNode ||\n this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.scrollTop = 0\n\n if (transition) {\n Util.reflow(this._element)\n }\n\n $(this._element).addClass(ClassName.SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(Event.SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)\n\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(Event.FOCUSIN) // Guard against infinite focus loop\n .on(Event.FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {\n if (event.which === ESCAPE_KEYCODE) {\n event.preventDefault()\n this.hide()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(Event.KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(Event.RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(ClassName.OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(Event.HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(ClassName.FADE)\n ? ClassName.FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = ClassName.BACKDROP\n\n if (animate) {\n this._backdrop.classList.add(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(Event.CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n if (this._config.backdrop === 'static') {\n this._element.focus()\n } else {\n this.hide()\n }\n })\n\n if (animate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(ClassName.SHOW)\n\n if (!callback) {\n return\n }\n\n if (!animate) {\n callback()\n return\n }\n\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName.SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing =\n this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))\n\n // Adjust fixed content padding\n $(fixedContent).each((index, element) => {\n const actualPadding = element.style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(stickyContent).each((index, element) => {\n const actualMargin = element.style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element)\n .data('margin-right', actualMargin)\n .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $(document.body).css('padding-right')\n $(document.body)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n\n $(document.body).addClass(ClassName.OPEN)\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n $(fixedContent).each((index, element) => {\n const padding = $(element).data('padding-right')\n $(element).removeData('padding-right')\n element.style.paddingRight = padding ? padding : ''\n })\n\n // Restore sticky content\n const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))\n $(elements).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $(document.body).data('padding-right')\n $(document.body).removeData('padding-right')\n document.body.style.paddingRight = padding ? padding : ''\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = ClassName.SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Default,\n ...$(this).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(Event.SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(Event.HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Modal._jQueryInterface\n$.fn[NAME].Constructor = Modal\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n}\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst DefaultType = {\n animation : 'boolean',\n template : 'string',\n title : '(string|element|function)',\n trigger : 'string',\n delay : '(number|object)',\n html : 'boolean',\n selector : '(string|boolean)',\n placement : '(string|function)',\n offset : '(number|string)',\n container : '(string|element|boolean)',\n fallbackPlacement : '(string|array)',\n boundary : '(string|element)'\n}\n\nconst AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n}\n\nconst Default = {\n animation : true,\n template : '
    ' +\n '
    ' +\n '
    ',\n trigger : 'hover focus',\n title : '',\n delay : 0,\n html : false,\n selector : false,\n placement : 'top',\n offset : 0,\n container : false,\n fallbackPlacement : 'flip',\n boundary : 'scrollParent'\n}\n\nconst HoverState = {\n SHOW : 'show',\n OUT : 'out'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TOOLTIP : '.tooltip',\n TOOLTIP_INNER : '.tooltip-inner',\n ARROW : '.arrow'\n}\n\nconst Trigger = {\n HOVER : 'hover',\n FOCUS : 'focus',\n CLICK : 'click',\n MANUAL : 'manual'\n}\n\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip {\n constructor(element, config) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal')\n\n if (this.tip) {\n $(this.tip).remove()\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.element = null\n this.config = null\n this.tip = null\n }\n\n show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const shadowRoot = Util.findShadowRoot(this.element)\n const isInTheDom = $.contains(\n shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(ClassName.FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this._getContainer()\n $(tip).data(this.constructor.DATA_KEY, this)\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container)\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED)\n\n this._popper = new Popper(this.element, tip, {\n placement: attachment,\n modifiers: {\n offset: {\n offset: this.config.offset\n },\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => this._handlePopperPlacementChange(data)\n })\n\n $(tip).addClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HoverState.OUT) {\n this._leave(null, this)\n }\n }\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(this.tip)\n\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n if (callback) {\n callback()\n }\n }\n\n $(this.element).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(tip).removeClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n this._activeTrigger[Trigger.CLICK] = false\n this._activeTrigger[Trigger.FOCUS] = false\n this._activeTrigger[Trigger.HOVER] = false\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(tip)\n\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())\n $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n setElementContent($element, content) {\n const html = this.config.html\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content)\n }\n } else {\n $element.text($(content).text())\n }\n } else {\n $element[html ? 'html' : 'text'](content)\n }\n }\n\n getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getContainer() {\n if (this.config.container === false) {\n return document.body\n }\n\n if (Util.isElement(this.config.container)) {\n return $(this.config.container)\n }\n\n return $(document).find(this.config.container)\n }\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== Trigger.MANUAL) {\n const eventIn = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(\n eventIn,\n this.config.selector,\n (event) => this._enter(event)\n )\n .on(\n eventOut,\n this.config.selector,\n (event) => this._leave(event)\n )\n }\n })\n\n $(this.element).closest('.modal').on(\n 'hide.bs.modal',\n () => {\n if (this.element) {\n this.hide()\n }\n }\n )\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this.element).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n const popperInstance = popperData.instance\n this.tip = popperInstance.popper\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(popperData.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n\n $(tip).removeClass(ClassName.FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tooltip._jQueryInterface\n$.fn[NAME].Constructor = Tooltip\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n}\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '
    ' +\n '
    ' +\n '

    ' +\n '
    '\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TITLE : '.popover-header',\n CONTENT : '.popover-body'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(Selector.TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(Selector.CONTENT), content)\n\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Popover._jQueryInterface\n$.fn[NAME].Constructor = Popover\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n}\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n offset : 10,\n method : 'auto',\n target : ''\n}\n\nconst DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n}\n\nconst Event = {\n ACTIVATE : `activate${EVENT_KEY}`,\n SCROLL : `scroll${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DROPDOWN_ITEM : 'dropdown-item',\n DROPDOWN_MENU : 'dropdown-menu',\n ACTIVE : 'active'\n}\n\nconst Selector = {\n DATA_SPY : '[data-spy=\"scroll\"]',\n ACTIVE : '.active',\n NAV_LIST_GROUP : '.nav, .list-group',\n NAV_LINKS : '.nav-link',\n NAV_ITEMS : '.nav-item',\n LIST_ITEMS : '.list-group-item',\n DROPDOWN : '.dropdown',\n DROPDOWN_ITEMS : '.dropdown-item',\n DROPDOWN_TOGGLE : '.dropdown-toggle'\n}\n\nconst OffsetMethod = {\n OFFSET : 'offset',\n POSITION : 'position'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy {\n constructor(element, config) {\n this._element = element\n this._scrollElement = element.tagName === 'BODY' ? window : element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${Selector.NAV_LINKS},` +\n `${this._config.target} ${Selector.LIST_ITEMS},` +\n `${this._config.target} ${Selector.DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? OffsetMethod.OFFSET : OffsetMethod.POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === OffsetMethod.POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = [].slice.call(document.querySelectorAll(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = document.querySelector(targetSelector)\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\n this._element = null\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.target !== 'string') {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset +\n scrollHeight -\n this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n const offsetLength = this._offsets.length\n for (let i = offsetLength; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector\n .split(',')\n .map((selector) => `${selector}[data-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))\n\n if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {\n $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)\n $link.addClass(ClassName.ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(ClassName.ACTIVE)\n // Set triggered links parents as active\n // With both
      and