builder 作成テスト





import bpy
import re
import os
import base64
import webbrowser
import datetime
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty
from bpy_extras.io_utils import ImportHelper

# ===================================================================
# 【定義】
# ===================================================================
ADDON_TITLE = "コンテナ builder"
PREFIX_VAL = "BUILDER_1001"
ADDON_CATEGORY = "コンテナ builder"

# ===================================================================
# 【名前初期値設定】
# ===================================================================
# 生成されるコンテナ名とサイドバー表示名の初期パターンを設定します。
# 「{datetime}」と記述された部分は、自動的に「YYYY_MMDD_HHMM」形式の現在日時に置換されます。
DEFAULT_NAME_TITLE = "コンテナ {datetime}"
DEFAULT_NAME_CATEGORY = "コンテナ {datetime}"

# ===================================================================
# 【リンク定義】 
# ===================================================================
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionadmillion/n/n2158226ce616>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

# =========================================================
# コンテナ生成ビルダー アドオン (Blender 5以降専用)
# =========================================================
bl_info = {
    "name": "コンテナ builder",
    "author": "AI",
    "version": (3, 2, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > コンテナ builder",
    "description": "エラーが起きないようにBase64変換してコンテナアドオンを生成します",
    "category": "3D View",
}

# =========================================================
# プロパティ取得ヘルパー
# =========================================================
def get_props(context):
    prop_name = f"{PREFIX_VAL.lower()}_props"
    return getattr(context.scene, prop_name, None)

# =========================================================
# カスタムゲッター/セッター
# =========================================================
def get_dynamic_default_title(self):
    if not self.get("container_title_stored", ""):
        # 日時指定フォーマットを「YYYY_MMDD_HHMM」に変更
        now_str = datetime.datetime.now().strftime("%Y_%m%d_%H%M")
        return DEFAULT_NAME_TITLE.replace("{datetime}", now_str)
    return self.get("container_title_stored", "")

def set_dynamic_default_title(self, value):
    self["container_title_stored"] = value

def get_dynamic_default_category(self):
    if not self.get("container_category_stored", ""):
        # 日時指定フォーマットを「YYYY_MMDD_HHMM」に変更
        now_str = datetime.datetime.now().strftime("%Y_%m%d_%H%M")
        return DEFAULT_NAME_CATEGORY.replace("{datetime}", now_str)
    return self.get("container_category_stored", "")

def set_dynamic_default_category(self, value):
    self["container_category_stored"] = value

# =========================================================
# データ構造
# =========================================================
class ZIONAD_BuilderItem(PropertyGroup):
    name: StringProperty()
    text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)

class ZIONAD_BuilderProps(PropertyGroup):
    items: CollectionProperty(type=ZIONAD_BuilderItem)
    active_index: IntProperty(default=0)
    
    container_title: StringProperty(
        name="コンテナ名",
        description="生成するアドオンの正式名称 (ADDON_TITLE)",
        get=get_dynamic_default_title,
        set=set_dynamic_default_title
    )
    container_category: StringProperty(
        name="サイドバー表示名",
        description="3Dビューのサイドバーに表示されるタブ名 (ADDON_CATEGORY)",
        get=get_dynamic_default_category,
        set=set_dynamic_default_category
    )
    
    custom_link_name: StringProperty(
        name="カスタムリンク名",
        description="コンテナ内に表示する独自の追加リンク1の名前 (空欄の場合は非表示)",
        default=""
    )
    custom_link_url: StringProperty(
        name="カスタムリンクURL",
        description="独自の追加リンク1のURL",
        default=""
    )

# =========================================================
# オペレーター: 追加・削除・一括インポート・アンインストール
# =========================================================
class ZIONAD_OT_BuilderAddClipboard(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.add_clipboard"
    bl_label = "クリップボードから取り込む"
    
    def execute(self, context):
        text = context.window_manager.clipboard
        if not text or len(text.strip()) == 0:
            self.report({'WARNING'}, "クリップボードが空です。")
            return {'CANCELLED'}
            
        props = get_props(context)
        if not props: return {'CANCELLED'}
            
        match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
        base_name = match.group(1) if match else f"Clipboard_Addon_{len(props.items)+1}"
        
        t = bpy.data.texts.new(name=f"{base_name}.py")
        t.write(text)
        
        item = props.items.add()
        item.name = base_name
        item.text_ptr = t
        props.active_index = len(props.items) - 1
        
        self.report({'INFO'}, f"{base_name} を追加しました")
        return {'FINISHED'}

class ZIONAD_OT_BuilderAddFile(Operator, ImportHelper):
    bl_idname = f"{PREFIX_VAL.lower()}.add_file"
    bl_label = "フォルダから.pyを選ぶ(複数可)"
    bl_description = "複数の.pyファイルを選択して一度に取り込みます (Shift/Ctrlキーで複数選択)"
    
    filename_ext = ".py"
    filter_glob: StringProperty(default="*.py", options={'HIDDEN'})
    
    files: CollectionProperty(
        name="File Path",
        type=bpy.types.OperatorFileListElement,
    )
    directory: StringProperty(
        subtype='DIR_PATH',
    )
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        added_count = 0
        
        if self.files:
            for file_elem in self.files:
                name = file_elem.name
                if not name:
                    continue
                    
                filepath = os.path.join(self.directory, name)
                
                try:
                    with open(filepath, 'r', encoding='utf-8') as f:
                        text = f.read()
                except Exception as e:
                    self.report({'ERROR'}, f"ファイルの読み込み失敗: {name} - {e}")
                    continue
                    
                t = bpy.data.texts.new(name=name)
                t.write(text)
                
                item = props.items.add()
                item.name = name.replace('.py', '')
                item.text_ptr = t
                added_count += 1
                
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"{added_count}個のファイルを追加しました")
        else:
            self.report({'WARNING'}, "ファイルが追加されませんでした")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderImportFromContainerText(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.import_container_text"
    bl_label = "コンテナテキストから一括復元"
    bl_description = "現在テキストエディタで開いているコンテナコードから、収録データをビルダーへ一括で取り込みます"
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        text_block = None
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR':
                text_block = area.spaces.active.text
                break
                
        if not text_block:
            self.report({'WARNING'}, "テキストエディタにテキストが開かれていません。")
            return {'CANCELLED'}
            
        code_str = text_block.as_string()
        
        match = re.search(r'STORED_CODES\s*=\s*\[(.*?)^\s*\]', code_str, re.DOTALL | re.MULTILINE)
        if not match:
            self.report({'WARNING'}, "テキスト中に 'STORED_CODES' が検出できませんでした。")
            return {'CANCELLED'}
            
        local_scope = {}
        try:
            exec(match.group(0), {}, local_scope)
        except Exception as e:
            self.report({'ERROR'}, f"解析エラー: {e}")
            return {'CANCELLED'}
            
        stored_codes = local_scope.get("STORED_CODES")
        if not stored_codes or not isinstance(stored_codes, list):
            self.report({'WARNING'}, "データリストの展開に失敗しました。")
            return {'CANCELLED'}
            
        added_count = 0
        for item in stored_codes:
            name = item.get("name")
            code_b64 = item.get("code_b64")
            if not name or not code_b64:
                continue
                
            try:
                raw_code = base64.b64decode(code_b64).decode('utf-8')
            except Exception as e:
                self.report({'ERROR'}, f"デコードエラー ({name}): {e}")
                continue
                
            text_name = f"{name}.py"
            if text_name in bpy.data.texts:
                t = bpy.data.texts[text_name]
                t.clear()
                t.write(raw_code)
            else:
                t = bpy.data.texts.new(text_name)
                t.write(raw_code)
                
            exists = False
            for b_item in props.items:
                if b_item.name == name:
                    b_item.text_ptr = t
                    exists = True
                    break
                    
            if not exists:
                new_item = props.items.add()
                new_item.name = name
                new_item.text_ptr = t
                
            added_count += 1
            
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"コンテナから {added_count} 個のアドオンデータをビルダーに復元しました")
        else:
            self.report({'WARNING'}, "復元可能なアドオンデータが見つかりませんでした。")
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderRemove(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_item"
    bl_label = "選択した項目を削除"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        item = props.items[idx]
        if item.text_ptr:
            bpy.data.texts.remove(item.text_ptr)
            
        props.items.remove(idx)
        props.active_index = max(0, min(idx, len(props.items) - 1))
        return {'FINISHED'}

class ZIONAD_OT_BuilderMoveItem(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.move_item"
    bl_label = "順番を移動"
    bl_description = "選択したアドオンの順番を上下に移動します"
    
    direction: StringProperty()
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        new_idx = idx - 1 if self.direction == 'UP' else idx + 1
        
        if 0 <= new_idx < len(props.items):
            props.items.move(idx, new_idx)
            props.active_index = new_idx
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderSortItems(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.sort_items"
    bl_label = "名前順にソート"
    bl_description = "追加されたアドオンをアルファベット順に並べ替えます"
    
    def execute(self, context):
        props = get_props(context)
        if not props or len(props.items) < 2: 
            return {'CANCELLED'}
            
        items_data = [(item.name, item.text_ptr) for item in props.items]
        items_data.sort(key=lambda x: x[0].lower())
        
        props.items.clear()
        for name, txt in items_data:
            new_item = props.items.add()
            new_item.name = name
            new_item.text_ptr = txt
            
        props.active_index = 0
        self.report({'INFO'}, "アドオンを名前順にソートしました")
        return {'FINISHED'}

class ZIONAD_OT_OpenUrl(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.open_url"
    bl_label = "リンクを開く"
    
    url: StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class ZIONAD_OT_RemoveBuilder(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_builder"
    bl_label = "Unregister Builder"
    bl_description = "このビルダーアドオンを無効化し、UIから消去します"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# オペレーター: コンテナコードの生成と出力 (メイン処理)
# =========================================================
class ZIONAD_OT_BuilderGenerate(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.generate"
    bl_label = "コンテナコードを出力"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items:
            self.report({'WARNING'}, "コードが一つも追加されていません。")
            return {'CANCELLED'}
            
        stored_codes_str = "STORED_CODES = [\n"
        for item in props.items:
            if not item.text_ptr: continue
            raw_code = item.text_ptr.as_string()
            safe_name = item.name
            
            encoded_bytes = base64.b64encode(raw_code.encode('utf-8'))
            encoded_str = encoded_bytes.decode('utf-8')
            chunks = [encoded_str[i:i+80] for i in range(0, len(encoded_str), 80)]
            formatted_b64 = '"\n        "'.join(chunks)
            
            stored_codes_str += f'    {{\n'
            stored_codes_str += f'        "name": "{safe_name}",\n'
            stored_codes_str += f'        "code_b64": (\n        "{formatted_b64}"\n        )\n'
            stored_codes_str += f'    }},\n'
        stored_codes_str += "]\n"
        
        current_time_str = datetime.datetime.now().strftime("%Y_%m%d_%H%M")
        container_prefix = f"CONT_{current_time_str.replace('_', '')}"
        
        target_title = props.container_title.strip()
        target_category = props.container_category.strip()
        
        c_link_name = props.custom_link_name.strip()
        c_link_url = props.custom_link_url.strip()

        template = f"""# ===================================================================
# 1. bl_info
# ===================================================================
bl_info = {{
    "name": "{target_title}",
    "author": "Generated by Builder",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > {target_category}",
    "description": "複数のアドオンコードを格納・展開するコンテナ (Blender 5以降専用)",
    "category": "3D View",
}}

# ===================================================================
# 2. import
# ===================================================================
import bpy
import base64
import webbrowser
from bpy.types import Operator, Panel

# ===================================================================
# 3. 共通定義
# ===================================================================
ADDON_TITLE = "{target_title}"
PREFIX_VAL = "{container_prefix}"
ADDON_CATEGORY = "{target_category}"
PREFIX = PREFIX_VAL.lower()

# ===================================================================
# 4. リンク定義
# ===================================================================
LINK_NAME_1 = "{c_link_name}"
LINK_URL_1 = "{c_link_url}"

LINK_NAME_2 = "{LINK_NAME_2}"
LINK_URL_2 = "{LINK_URL_2}"

LINK_NAME_3 = "{LINK_NAME_3}"
LINK_URL_3 = "{LINK_URL_3}"

LINK_NAME_4 = "{LINK_NAME_4}"
LINK_URL_4 = "{LINK_URL_4}"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================================================
# 5. 共通クラス
# ===================================================================
class {container_prefix}_OT_OpenUrl(Operator):
    bl_idname = f"{{PREFIX}}.open_url"
    bl_label = "リンクを開く"
    
    url: bpy.props.StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {{'FINISHED'}}

# ===================================================================
# 6. アドオン固有機能
# ===================================================================
{stored_codes_str}

class {container_prefix}_OT_OpenCode(Operator):
    bl_idname = f"{{PREFIX}}.open_stored_code"
    bl_label = "テキストエディタで開く"
    
    code_index: bpy.props.IntProperty()
    
    def execute(self, context):
        item = STORED_CODES[self.code_index]
        text_name = item["name"] + ".py"
        
        code_text = base64.b64decode(item["code_b64"]).decode('utf-8')
        
        if text_name in bpy.data.texts:
            t = bpy.data.texts[text_name]
            t.clear()
            t.write(code_text)
        else:
            t = bpy.data.texts.new(text_name)
            t.use_fake_user = True
            t.write(code_text)
            
        editor_found = False
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = t
                    editor_found = True
                    break
            if editor_found:
                break
                
        if not editor_found:
            self.report({{'INFO'}}, f"'{{text_name}}' を生成しました。Text Editorを開いてください。")
        return {{'FINISHED'}}

class {container_prefix}_OT_RemoveContainer(Operator):
    bl_idname = f"{{PREFIX}}.remove_container"
    bl_label = "Unregister Container"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {{'FINISHED'}}

class {container_prefix}_PT_ContainerPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{container_prefix}_PT_container_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if not STORED_CODES:
            box.label(text="コードがありません")
        else:
            for i, item in enumerate(STORED_CODES):
                row = box.row(align=True)
                row.label(text=str(i + 1) + ": " + item["name"], icon='FILE_SCRIPT')
                
                # 右寄せサブレイアウトにより、「展開」ボタンを常に最小サイズで右寄せ維持します
                op_row = row.row(align=True)
                op_row.alignment = 'RIGHT'
                
                op = op_row.operator(f"{{PREFIX}}.open_stored_code", text="展開", icon='TEXT')
                op.code_index = i

class {container_prefix}_PT_ContainerLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{container_prefix}_PT_container_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        if LINK_NAME_1 and LINK_URL_1:
            op1 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_1, icon='URL')
            op1.url = LINK_URL_1
        
        if LINK_NAME_2:
            op2 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_2, icon='URL')
            op2.url = LINK_URL_2
        
        if LINK_NAME_3:
            op3 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_3, icon='URL')
            op3.url = LINK_URL_3
            
        if LINK_NAME_4:
            op4 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_4, icon='URL')
            op4.url = LINK_URL_4

class {container_prefix}_PT_ContainerRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{container_prefix}_PT_container_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {{'DEFAULT_CLOSED'}}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このコンテナアドオンを消去します", icon='ERROR')
        box.operator(f"{{PREFIX}}.remove_container", icon='CANCEL')

# ===================================================================
# 7. register / unregister
# ===================================================================
classes = (
    {container_prefix}_OT_OpenUrl,
    {container_prefix}_OT_OpenCode,
    {container_prefix}_OT_RemoveContainer,
    {container_prefix}_PT_ContainerPanel,
    {container_prefix}_PT_ContainerLinksPanel,
    {container_prefix}_PT_ContainerRemovePanel
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
"""
        out_name = f"{target_title}.py".replace(" ", "_").replace(" ", "_")
        if out_name in bpy.data.texts:
            out_text = bpy.data.texts[out_name]
            out_text.clear()
        else:
            out_text = bpy.data.texts.new(out_name)
            
        out_text.write(template)
        
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    
        self.report({'INFO'}, f"コンテナコード '{out_name}' を生成しました")
        return {'FINISHED'}

# =========================================================
# UI パネル (UIListを含む)
# =========================================================
class ZIONAD_UL_BuilderList(UIList):
    bl_idname = f"{PREFIX_VAL}_UL_builder_list"
    
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index=0, flt_flag=0):
        layout.label(text=f"{index + 1}: {item.name}", icon='FILE_SCRIPT')

class ZIONAD_PT_BuilderPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_builder_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = get_props(context)
        if not props: return
        
        box = layout.box()
        box.label(text="1. 取り込み操作:")
        box.operator(ZIONAD_OT_BuilderAddFile.bl_idname, icon='FILEBROWSER')
        box.operator(ZIONAD_OT_BuilderAddClipboard.bl_idname, icon='PASTEDOWN')
        box.operator(ZIONAD_OT_BuilderImportFromContainerText.bl_idname, icon='TEXT')
        
        layout.separator()
        layout.label(text="2. コード一覧(ソート・順序変更):")
        
        row = layout.row()
        row.template_list(ZIONAD_UL_BuilderList.bl_idname, "", props, "items", props, "active_index", rows=5)
        
        col = row.column(align=True)
        col.operator(ZIONAD_OT_BuilderRemove.bl_idname, text="", icon='REMOVE')
        col.separator()
        op_up = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_UP')
        op_up.direction = 'UP'
        op_down = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_DOWN')
        op_down.direction = 'DOWN'
        
        layout.operator(ZIONAD_OT_BuilderSortItems.bl_idname, icon='SORTALPHA')
        
        # 3. コンテナ設定
        layout.separator()
        layout.label(text="3. コンテナ設定:")
        box_cfg = layout.box()
        box_cfg.prop(props, "container_title")
        box_cfg.prop(props, "container_category")
        
        box_cfg.separator()
        box_cfg.label(text="追加カスタムリンク (任意項目):", icon='URL')
        box_cfg.prop(props, "custom_link_name", text="リンク1 タイトル")
        box_cfg.prop(props, "custom_link_url", text="リンク1 URL")
        
        # 4. アドオンの作成
        layout.separator()
        layout.label(text="4. アドオンの作成:")
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(ZIONAD_OT_BuilderGenerate.bl_idname, icon='FILE_TICK')

class ZIONAD_PT_BuilderLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_builder_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        op2 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_2, icon='URL')
        op2.url = LINK_URL_2
        
        op3 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_3, icon='URL')
        op3.url = LINK_URL_3

        op4 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_4, icon='URL')
        op4.url = LINK_URL_4

class ZIONAD_PT_BuilderRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_builder_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このビルダーアドオンを消去します", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveBuilder.bl_idname, icon='CANCEL')

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_BuilderItem,
    ZIONAD_BuilderProps,
    ZIONAD_OT_BuilderAddClipboard,
    ZIONAD_OT_BuilderAddFile,
    ZIONAD_OT_BuilderImportFromContainerText,
    ZIONAD_OT_BuilderRemove,
    ZIONAD_OT_BuilderMoveItem,
    ZIONAD_OT_BuilderSortItems,
    ZIONAD_OT_OpenUrl,
    ZIONAD_OT_RemoveBuilder,
    ZIONAD_OT_BuilderGenerate,
    ZIONAD_UL_BuilderList,
    ZIONAD_PT_BuilderPanel,
    ZIONAD_PT_BuilderLinksPanel,
    ZIONAD_PT_BuilderRemovePanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    prop_name = f"{PREFIX_VAL.lower()}_props"
    setattr(bpy.types.Scene, prop_name, PointerProperty(type=ZIONAD_BuilderProps))

def unregister():
    prop_name = f"{PREFIX_VAL.lower()}_props"
    if hasattr(bpy.types.Scene, prop_name):
        delattr(bpy.types.Scene, prop_name)
        
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
import bpy
import re
import os
import base64
import webbrowser
import datetime
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty
from bpy_extras.io_utils import ImportHelper

# ===================================================================
# 【定義】
# ===================================================================
ADDON_TITLE = "コンテナ builder"
PREFIX_VAL = "BUILDER_1001"
ADDON_CATEGORY = "コンテナ builder"

# ===================================================================
# 【名前初期値設定】
# ===================================================================
DEFAULT_NAME_TITLE = "コンテナ {datetime}"
DEFAULT_NAME_CATEGORY = "コンテナ {datetime}"

# ===================================================================
# 【リンク定義】 
# ===================================================================
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionadmillion/n/n2158226ce616>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

# =========================================================
# コンテナ生成ビルダー アドオン (Blender 5以降専用)
# =========================================================
bl_info = {
    "name": "コンテナ builder",
    "author": "AI",
    "version": (3, 1, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > コンテナ builder",
    "description": "エラーが起きないようにBase64変換してコンテナアドオンを生成します",
    "category": "3D View",
}

# =========================================================
# プロパティ取得ヘルパー
# =========================================================
def get_props(context):
    prop_name = f"{PREFIX_VAL.lower()}_props"
    return getattr(context.scene, prop_name, None)

# =========================================================
# カスタムゲッター/セッター
# =========================================================
def get_dynamic_default_title(self):
    if not self.get("container_title_stored", ""):
        now_str = datetime.datetime.now().strftime("%Y%m%d%H%M")
        return DEFAULT_NAME_TITLE.replace("{datetime}", now_str)
    return self.get("container_title_stored", "")

def set_dynamic_default_title(self, value):
    self["container_title_stored"] = value

def get_dynamic_default_category(self):
    if not self.get("container_category_stored", ""):
        now_str = datetime.datetime.now().strftime("%Y%m%d%H%M")
        return DEFAULT_NAME_CATEGORY.replace("{datetime}", now_str)
    return self.get("container_category_stored", "")

def set_dynamic_default_category(self, value):
    self["container_category_stored"] = value

# =========================================================
# データ構造
# =========================================================
class ZIONAD_BuilderItem(PropertyGroup):
    name: StringProperty()
    text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)

class ZIONAD_BuilderProps(PropertyGroup):
    items: CollectionProperty(type=ZIONAD_BuilderItem)
    active_index: IntProperty(default=0)
    
    container_title: StringProperty(
        name="コンテナ名",
        description="生成するアドオンの正式名称 (ADDON_TITLE)",
        get=get_dynamic_default_title,
        set=set_dynamic_default_title
    )
    container_category: StringProperty(
        name="サイドバー表示名",
        description="3Dビューのサイドバーに表示されるタブ名 (ADDON_CATEGORY)",
        get=get_dynamic_default_category,
        set=set_dynamic_default_category
    )
    
    custom_link_name: StringProperty(
        name="カスタムリンク名",
        description="コンテナ内に表示する独自の追加リンク1の名前 (空欄の場合は非表示)",
        default=""
    )
    custom_link_url: StringProperty(
        name="カスタムリンクURL",
        description="独自の追加リンク1のURL",
        default=""
    )

# =========================================================
# オペレーター: 追加・削除・一括インポート・アンインストール
# =========================================================
class ZIONAD_OT_BuilderAddClipboard(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.add_clipboard"
    bl_label = "クリップボードから取り込む"
    
    def execute(self, context):
        text = context.window_manager.clipboard
        if not text or len(text.strip()) == 0:
            self.report({'WARNING'}, "クリップボードが空です。")
            return {'CANCELLED'}
            
        props = get_props(context)
        if not props: return {'CANCELLED'}
            
        match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
        base_name = match.group(1) if match else f"Clipboard_Addon_{len(props.items)+1}"
        
        t = bpy.data.texts.new(name=f"{base_name}.py")
        t.write(text)
        
        item = props.items.add()
        item.name = base_name
        item.text_ptr = t
        props.active_index = len(props.items) - 1
        
        self.report({'INFO'}, f"{base_name} を追加しました")
        return {'FINISHED'}

class ZIONAD_OT_BuilderAddFile(Operator, ImportHelper):
    bl_idname = f"{PREFIX_VAL.lower()}.add_file"
    bl_label = "フォルダから.pyを選ぶ(複数可)"
    bl_description = "複数の.pyファイルを選択して一度に取り込みます (Shift/Ctrlキーで複数選択)"
    
    filename_ext = ".py"
    filter_glob: StringProperty(default="*.py", options={'HIDDEN'})
    
    files: CollectionProperty(
        name="File Path",
        type=bpy.types.OperatorFileListElement,
    )
    directory: StringProperty(
        subtype='DIR_PATH',
    )
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        added_count = 0
        
        if self.files:
            for file_elem in self.files:
                name = file_elem.name
                if not name:
                    continue
                    
                filepath = os.path.join(self.directory, name)
                
                try:
                    with open(filepath, 'r', encoding='utf-8') as f:
                        text = f.read()
                except Exception as e:
                    self.report({'ERROR'}, f"ファイルの読み込み失敗: {name} - {e}")
                    continue
                    
                t = bpy.data.texts.new(name=name)
                t.write(text)
                
                item = props.items.add()
                item.name = name.replace('.py', '')
                item.text_ptr = t
                added_count += 1
                
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"{added_count}個のファイルを追加しました")
        else:
            self.report({'WARNING'}, "ファイルが追加されませんでした")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderImportFromContainerText(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.import_container_text"
    bl_label = "コンテナテキストから一括復元"
    bl_description = "現在テキストエディタで開いているコンテナコードから、収録データをビルダーへ一括で取り込みます"
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        # アクティブなテキストブロックを取得
        text_block = None
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR':
                text_block = area.spaces.active.text
                break
                
        if not text_block:
            self.report({'WARNING'}, "テキストエディタにテキストが開かれていません。")
            return {'CANCELLED'}
            
        code_str = text_block.as_string()
        
        # STORED_CODESの定義ブロックを正規表現で抽出
        match = re.search(r'STORED_CODES\s*=\s*\[(.*?)^\s*\]', code_str, re.DOTALL | re.MULTILINE)
        if not match:
            self.report({'WARNING'}, "テキスト中に 'STORED_CODES' が検出できませんでした。")
            return {'CANCELLED'}
            
        local_scope = {}
        try:
            # 抽出したリスト構造をPythonコードとして実行
            exec(match.group(0), {}, local_scope)
        except Exception as e:
            self.report({'ERROR'}, f"解析エラー: {e}")
            return {'CANCELLED'}
            
        stored_codes = local_scope.get("STORED_CODES")
        if not stored_codes or not isinstance(stored_codes, list):
            self.report({'WARNING'}, "データリストの展開に失敗しました。")
            return {'CANCELLED'}
            
        added_count = 0
        for item in stored_codes:
            name = item.get("name")
            code_b64 = item.get("code_b64")
            if not name or not code_b64:
                continue
                
            try:
                raw_code = base64.b64decode(code_b64).decode('utf-8')
            except Exception as e:
                self.report({'ERROR'}, f"デコードエラー ({name}): {e}")
                continue
                
            text_name = f"{name}.py"
            if text_name in bpy.data.texts:
                t = bpy.data.texts[text_name]
                t.clear()
                t.write(raw_code)
            else:
                t = bpy.data.texts.new(text_name)
                t.write(raw_code)
                
            # すでにビルダーに同一名称があるか確認し、上書き・新規追加
            exists = False
            for b_item in props.items:
                if b_item.name == name:
                    b_item.text_ptr = t
                    exists = True
                    break
                    
            if not exists:
                new_item = props.items.add()
                new_item.name = name
                new_item.text_ptr = t
                
            added_count += 1
            
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"コンテナから {added_count} 個のアドオンデータをビルダーに復元しました")
        else:
            self.report({'WARNING'}, "復元可能なアドオンデータが見つかりませんでした。")
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderRemove(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_item"
    bl_label = "選択した項目を削除"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        item = props.items[idx]
        if item.text_ptr:
            bpy.data.texts.remove(item.text_ptr)
            
        props.items.remove(idx)
        props.active_index = max(0, min(idx, len(props.items) - 1))
        return {'FINISHED'}

class ZIONAD_OT_BuilderMoveItem(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.move_item"
    bl_label = "順番を移動"
    bl_description = "選択したアドオンの順番を上下に移動します"
    
    direction: StringProperty()
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        new_idx = idx - 1 if self.direction == 'UP' else idx + 1
        
        if 0 <= new_idx < len(props.items):
            props.items.move(idx, new_idx)
            props.active_index = new_idx
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderSortItems(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.sort_items"
    bl_label = "名前順にソート"
    bl_description = "追加されたアドオンをアルファベット順に並べ替えます"
    
    def execute(self, context):
        props = get_props(context)
        if not props or len(props.items) < 2: 
            return {'CANCELLED'}
            
        items_data = [(item.name, item.text_ptr) for item in props.items]
        items_data.sort(key=lambda x: x[0].lower())
        
        props.items.clear()
        for name, txt in items_data:
            new_item = props.items.add()
            new_item.name = name
            new_item.text_ptr = txt
            
        props.active_index = 0
        self.report({'INFO'}, "アドオンを名前順にソートしました")
        return {'FINISHED'}

class ZIONAD_OT_OpenUrl(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.open_url"
    bl_label = "リンクを開く"
    
    url: StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class ZIONAD_OT_RemoveBuilder(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_builder"
    bl_label = "Unregister Builder"
    bl_description = "このビルダーアドオンを無効化し、UIから消去します"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# オペレーター: コンテナコードの生成と出力 (メイン処理)
# =========================================================
class ZIONAD_OT_BuilderGenerate(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.generate"
    bl_label = "コンテナコードを出力"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items:
            self.report({'WARNING'}, "コードが一つも追加されていません。")
            return {'CANCELLED'}
            
        stored_codes_str = "STORED_CODES = [\n"
        for item in props.items:
            if not item.text_ptr: continue
            raw_code = item.text_ptr.as_string()
            safe_name = item.name
            
            encoded_bytes = base64.b64encode(raw_code.encode('utf-8'))
            encoded_str = encoded_bytes.decode('utf-8')
            chunks = [encoded_str[i:i+80] for i in range(0, len(encoded_str), 80)]
            formatted_b64 = '"\n        "'.join(chunks)
            
            stored_codes_str += f'    {{\n'
            stored_codes_str += f'        "name": "{safe_name}",\n'
            stored_codes_str += f'        "code_b64": (\n        "{formatted_b64}"\n        )\n'
            stored_codes_str += f'    }},\n'
        stored_codes_str += "]\n"
        
        current_time_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        container_prefix = f"CONT_{current_time_str}"
        
        target_title = props.container_title.strip()
        target_category = props.container_category.strip()
        
        c_link_name = props.custom_link_name.strip()
        c_link_url = props.custom_link_url.strip()

        template = f"""# ===================================================================
# 1. bl_info
# ===================================================================
bl_info = {{
    "name": "{target_title}",
    "author": "Generated by Builder",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > {target_category}",
    "description": "複数のアドオンコードを格納・展開するコンテナ (Blender 5以降専用)",
    "category": "3D View",
}}

# ===================================================================
# 2. import
# ===================================================================
import bpy
import base64
import webbrowser
from bpy.types import Operator, Panel

# ===================================================================
# 3. 共通定義
# ===================================================================
ADDON_TITLE = "{target_title}"
PREFIX_VAL = "{container_prefix}"
ADDON_CATEGORY = "{target_category}"
PREFIX = PREFIX_VAL.lower()

# ===================================================================
# 4. リンク定義
# ===================================================================
LINK_NAME_1 = "{c_link_name}"
LINK_URL_1 = "{c_link_url}"

LINK_NAME_2 = "{LINK_NAME_2}"
LINK_URL_2 = "{LINK_URL_2}"

LINK_NAME_3 = "{LINK_NAME_3}"
LINK_URL_3 = "{LINK_URL_3}"

LINK_NAME_4 = "{LINK_NAME_4}"
LINK_URL_4 = "{LINK_URL_4}"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================================================
# 5. 共通クラス
# ===================================================================
class {container_prefix}_OT_OpenUrl(Operator):
    bl_idname = f"{{PREFIX}}.open_url"
    bl_label = "リンクを開く"
    
    url: bpy.props.StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {{'FINISHED'}}

# ===================================================================
# 6. アドオン固有機能
# ===================================================================
{stored_codes_str}

class {container_prefix}_OT_OpenCode(Operator):
    bl_idname = f"{{PREFIX}}.open_stored_code"
    bl_label = "テキストエディタで開く"
    
    code_index: bpy.props.IntProperty()
    
    def execute(self, context):
        item = STORED_CODES[self.code_index]
        text_name = item["name"] + ".py"
        
        code_text = base64.b64decode(item["code_b64"]).decode('utf-8')
        
        if text_name in bpy.data.texts:
            t = bpy.data.texts[text_name]
            t.clear()
            t.write(code_text)
        else:
            t = bpy.data.texts.new(text_name)
            t.use_fake_user = True
            t.write(code_text)
            
        editor_found = False
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = t
                    editor_found = True
                    break
            if editor_found:
                break
                
        if not editor_found:
            self.report({{'INFO'}}, f"'{{text_name}}' を生成しました。Text Editorを開いてください。")
        return {{'FINISHED'}}

class {container_prefix}_OT_RemoveContainer(Operator):
    bl_idname = f"{{PREFIX}}.remove_container"
    bl_label = "Unregister Container"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {{'FINISHED'}}

class {container_prefix}_PT_ContainerPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{container_prefix}_PT_container_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if not STORED_CODES:
            box.label(text="コードがありません")
        else:
            for i, item in enumerate(STORED_CODES):
                row = box.row(align=True)
                row.label(text=str(i + 1) + ": " + item["name"], icon='FILE_SCRIPT')
                
                # 右寄せサブレイアウトにより、「展開」ボタンを常に最小サイズで右寄せ維持します
                op_row = row.row(align=True)
                op_row.alignment = 'RIGHT'
                
                op = op_row.operator(f"{{PREFIX}}.open_stored_code", text="展開", icon='TEXT')
                op.code_index = i

class {container_prefix}_PT_ContainerLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{container_prefix}_PT_container_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        if LINK_NAME_1 and LINK_URL_1:
            op1 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_1, icon='URL')
            op1.url = LINK_URL_1
        
        if LINK_NAME_2:
            op2 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_2, icon='URL')
            op2.url = LINK_URL_2
        
        if LINK_NAME_3:
            op3 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_3, icon='URL')
            op3.url = LINK_URL_3
            
        if LINK_NAME_4:
            op4 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_4, icon='URL')
            op4.url = LINK_URL_4

class {container_prefix}_PT_ContainerRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{container_prefix}_PT_container_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {{'DEFAULT_CLOSED'}}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このコンテナアドオンを消去します", icon='ERROR')
        box.operator(f"{{PREFIX}}.remove_container", icon='CANCEL')

# ===================================================================
# 7. register / unregister
# ===================================================================
classes = (
    {container_prefix}_OT_OpenUrl,
    {container_prefix}_OT_OpenCode,
    {container_prefix}_OT_RemoveContainer,
    {container_prefix}_PT_ContainerPanel,
    {container_prefix}_PT_ContainerLinksPanel,
    {container_prefix}_PT_ContainerRemovePanel
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
"""
        out_name = f"{target_title}.py".replace(" ", "_").replace(" ", "_")
        if out_name in bpy.data.texts:
            out_text = bpy.data.texts[out_name]
            out_text.clear()
        else:
            out_text = bpy.data.texts.new(out_name)
            
        out_text.write(template)
        
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    
        self.report({'INFO'}, f"コンテナコード '{out_name}' を生成しました")
        return {'FINISHED'}

# =========================================================
# UI パネル (UIListを含む)
# =========================================================
class ZIONAD_UL_BuilderList(UIList):
    bl_idname = f"{PREFIX_VAL}_UL_builder_list"
    
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index=0, flt_flag=0):
        layout.label(text=f"{index + 1}: {item.name}", icon='FILE_SCRIPT')

class ZIONAD_PT_BuilderPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_builder_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = get_props(context)
        if not props: return
        
        box = layout.box()
        box.label(text="1. 取り込み操作:")
        box.operator(ZIONAD_OT_BuilderAddFile.bl_idname, icon='FILEBROWSER')
        box.operator(ZIONAD_OT_BuilderAddClipboard.bl_idname, icon='PASTEDOWN')
        box.operator(ZIONAD_OT_BuilderImportFromContainerText.bl_idname, icon='TEXT')
        
        layout.separator()
        layout.label(text="2. コード一覧(ソート・順序変更):")
        
        row = layout.row()
        row.template_list(ZIONAD_UL_BuilderList.bl_idname, "", props, "items", props, "active_index", rows=5)
        
        col = row.column(align=True)
        col.operator(ZIONAD_OT_BuilderRemove.bl_idname, text="", icon='REMOVE')
        col.separator()
        op_up = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_UP')
        op_up.direction = 'UP'
        op_down = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_DOWN')
        op_down.direction = 'DOWN'
        
        layout.operator(ZIONAD_OT_BuilderSortItems.bl_idname, icon='SORTALPHA')
        
        # 3. コンテナ設定
        layout.separator()
        layout.label(text="3. コンテナ設定:")
        box_cfg = layout.box()
        box_cfg.prop(props, "container_title")
        box_cfg.prop(props, "container_category")
        
        box_cfg.separator()
        box_cfg.label(text="追加カスタムリンク (任意項目):", icon='URL')
        box_cfg.prop(props, "custom_link_name", text="リンク1 タイトル")
        box_cfg.prop(props, "custom_link_url", text="リンク1 URL")
        
        # 4. アドオンの作成
        layout.separator()
        layout.label(text="4. アドオンの作成:")
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(ZIONAD_OT_BuilderGenerate.bl_idname, icon='FILE_TICK')

class ZIONAD_PT_BuilderLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_builder_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        op2 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_2, icon='URL')
        op2.url = LINK_URL_2
        
        op3 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_3, icon='URL')
        op3.url = LINK_URL_3

        op4 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_4, icon='URL')
        op4.url = LINK_URL_4

class ZIONAD_PT_BuilderRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_builder_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このビルダーアドオンを消去します", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveBuilder.bl_idname, icon='CANCEL')

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_BuilderItem,
    ZIONAD_BuilderProps,
    ZIONAD_OT_BuilderAddClipboard,
    ZIONAD_OT_BuilderAddFile,
    ZIONAD_OT_BuilderImportFromContainerText,
    ZIONAD_OT_BuilderRemove,
    ZIONAD_OT_BuilderMoveItem,
    ZIONAD_OT_BuilderSortItems,
    ZIONAD_OT_OpenUrl,
    ZIONAD_OT_RemoveBuilder,
    ZIONAD_OT_BuilderGenerate,
    ZIONAD_UL_BuilderList,
    ZIONAD_PT_BuilderPanel,
    ZIONAD_PT_BuilderLinksPanel,
    ZIONAD_PT_BuilderRemovePanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    prop_name = f"{PREFIX_VAL.lower()}_props"
    setattr(bpy.types.Scene, prop_name, PointerProperty(type=ZIONAD_BuilderProps))

def unregister():
    prop_name = f"{PREFIX_VAL.lower()}_props"
    if hasattr(bpy.types.Scene, prop_name):
        delattr(bpy.types.Scene, prop_name)
        
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
作成した コンテナから 一括で builder にデータをコピーしたい
import bpy
import re
import os
import base64
import webbrowser
import datetime
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty
from bpy_extras.io_utils import ImportHelper

# ===================================================================
# 【定義】
# ===================================================================
ADDON_TITLE = "コンテナ builder"
PREFIX_VAL = "BUILDER_1001"
ADDON_CATEGORY = "コンテナ builder"

# ===================================================================
# 【名前初期値設定】
# ===================================================================
# 生成されるコンテナ名とサイドバー表示名の初期パターンを設定します。
# 「{datetime}」と記述された部分は、自動的に「YYYYMMDDHHMM」形式の現在日時に置換されます。
DEFAULT_NAME_TITLE = "コンテナ {datetime}"
DEFAULT_NAME_CATEGORY = "コンテナ {datetime}"

# ===================================================================
# 【リンク定義】 
# ===================================================================
# 1番をコンテナ作成時のカスタム入力用に空け、旧1〜3番を2〜4番にシフト
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionadmillion/n/n2158226ce616>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

# =========================================================
# コンテナ生成ビルダー アドオン (Blender 5以降専用)
# =========================================================
bl_info = {
    "name": "コンテナ builder",
    "author": "AI",
    "version": (2, 9, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > コンテナ builder",
    "description": "エラーが起きないようにBase64変換してコンテナアドオンを生成します",
    "category": "3D View",
}

# =========================================================
# プロパティ取得ヘルパー
# =========================================================
def get_props(context):
    prop_name = f"{PREFIX_VAL.lower()}_props"
    return getattr(context.scene, prop_name, None)

# =========================================================
# カスタムゲッター/セッター (動的な日時デフォルト値用)
# =========================================================
def get_dynamic_default_title(self):
    if not self.get("container_title_stored", ""):
        now_str = datetime.datetime.now().strftime("%Y%m%d%H%M")
        return DEFAULT_NAME_TITLE.replace("{datetime}", now_str)
    return self.get("container_title_stored", "")

def set_dynamic_default_title(self, value):
    self["container_title_stored"] = value

def get_dynamic_default_category(self):
    if not self.get("container_category_stored", ""):
        now_str = datetime.datetime.now().strftime("%Y%m%d%H%M")
        return DEFAULT_NAME_CATEGORY.replace("{datetime}", now_str)
    return self.get("container_category_stored", "")

def set_dynamic_default_category(self, value):
    self["container_category_stored"] = value

# =========================================================
# データ構造
# =========================================================
class ZIONAD_BuilderItem(PropertyGroup):
    name: StringProperty()
    text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)

class ZIONAD_BuilderProps(PropertyGroup):
    items: CollectionProperty(type=ZIONAD_BuilderItem)
    active_index: IntProperty(default=0)
    
    # 動的な初期値を持つプロパティ
    container_title: StringProperty(
        name="コンテナ名",
        description="生成するアドオンの正式名称 (ADDON_TITLE)",
        get=get_dynamic_default_title,
        set=set_dynamic_default_title
    )
    container_category: StringProperty(
        name="サイドバー表示名",
        description="3Dビューのサイドバーに表示されるタブ名 (ADDON_CATEGORY)",
        get=get_dynamic_default_category,
        set=set_dynamic_default_category
    )
    
    # 新設: カスタムリンク1設定用プロパティ
    custom_link_name: StringProperty(
        name="カスタムリンク名",
        description="コンテナ内に表示する独自の追加リンク1の名前 (空欄の場合は非表示)",
        default=""
    )
    custom_link_url: StringProperty(
        name="カスタムリンクURL",
        description="独自の追加リンク1のURL",
        default=""
    )

# =========================================================
# オペレーター: 追加・削除・リンク・アンインストール
# =========================================================
class ZIONAD_OT_BuilderAddClipboard(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.add_clipboard"
    bl_label = "クリップボードから取り込む"
    
    def execute(self, context):
        text = context.window_manager.clipboard
        if not text or len(text.strip()) == 0:
            self.report({'WARNING'}, "クリップボードが空です。")
            return {'CANCELLED'}
            
        props = get_props(context)
        if not props: return {'CANCELLED'}
            
        match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
        base_name = match.group(1) if match else f"Clipboard_Addon_{len(props.items)+1}"
        
        t = bpy.data.texts.new(name=f"{base_name}.py")
        t.write(text)
        
        item = props.items.add()
        item.name = base_name
        item.text_ptr = t
        props.active_index = len(props.items) - 1
        
        self.report({'INFO'}, f"{base_name} を追加しました")
        return {'FINISHED'}

class ZIONAD_OT_BuilderAddFile(Operator, ImportHelper):
    bl_idname = f"{PREFIX_VAL.lower()}.add_file"
    bl_label = "フォルダから.pyを選ぶ(複数可)"
    bl_description = "複数の.pyファイルを選択して一度に取り込みます (Shift/Ctrlキーで複数選択)"
    
    filename_ext = ".py"
    filter_glob: StringProperty(default="*.py", options={'HIDDEN'})
    
    files: CollectionProperty(
        name="File Path",
        type=bpy.types.OperatorFileListElement,
    )
    directory: StringProperty(
        subtype='DIR_PATH',
    )
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        added_count = 0
        
        if self.files:
            for file_elem in self.files:
                name = file_elem.name
                if not name:
                    continue
                    
                filepath = os.path.join(self.directory, name)
                
                try:
                    with open(filepath, 'r', encoding='utf-8') as f:
                        text = f.read()
                except Exception as e:
                    self.report({'ERROR'}, f"ファイルの読み込み失敗: {name} - {e}")
                    continue
                    
                t = bpy.data.texts.new(name=name)
                t.write(text)
                
                item = props.items.add()
                item.name = name.replace('.py', '')
                item.text_ptr = t
                added_count += 1
                
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"{added_count}個のファイルを追加しました")
        else:
            self.report({'WARNING'}, "ファイルが追加されませんでした")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderRemove(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_item"
    bl_label = "選択した項目を削除"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        item = props.items[idx]
        if item.text_ptr:
            bpy.data.texts.remove(item.text_ptr)
            
        props.items.remove(idx)
        props.active_index = max(0, min(idx, len(props.items) - 1))
        return {'FINISHED'}

class ZIONAD_OT_BuilderMoveItem(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.move_item"
    bl_label = "順番を移動"
    bl_description = "選択したアドオンの順番を上下に移動します"
    
    direction: StringProperty()
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        new_idx = idx - 1 if self.direction == 'UP' else idx + 1
        
        if 0 <= new_idx < len(props.items):
            props.items.move(idx, new_idx)
            props.active_index = new_idx
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderSortItems(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.sort_items"
    bl_label = "名前順にソート"
    bl_description = "追加されたアドオンをアルファベット順に並べ替えます"
    
    def execute(self, context):
        props = get_props(context)
        if not props or len(props.items) < 2: 
            return {'CANCELLED'}
            
        items_data = [(item.name, item.text_ptr) for item in props.items]
        items_data.sort(key=lambda x: x[0].lower())
        
        props.items.clear()
        for name, txt in items_data:
            new_item = props.items.add()
            new_item.name = name
            new_item.text_ptr = txt
            
        props.active_index = 0
        self.report({'INFO'}, "アドオンを名前順にソートしました")
        return {'FINISHED'}

class ZIONAD_OT_OpenUrl(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.open_url"
    bl_label = "リンクを開く"
    
    url: StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class ZIONAD_OT_RemoveBuilder(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_builder"
    bl_label = "Unregister Builder"
    bl_description = "このビルダーアドオンを無効化し、UIから消去します"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# オペレーター: コンテナコードの生成と出力 (メイン処理)
# =========================================================
class ZIONAD_OT_BuilderGenerate(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.generate"
    bl_label = "コンテナコードを出力"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items:
            self.report({'WARNING'}, "コードが一つも追加されていません。")
            return {'CANCELLED'}
            
        stored_codes_str = "STORED_CODES = [\n"
        for item in props.items:
            if not item.text_ptr: continue
            raw_code = item.text_ptr.as_string()
            safe_name = item.name
            
            encoded_bytes = base64.b64encode(raw_code.encode('utf-8'))
            encoded_str = encoded_bytes.decode('utf-8')
            chunks = [encoded_str[i:i+80] for i in range(0, len(encoded_str), 80)]
            formatted_b64 = '"\n        "'.join(chunks)
            
            stored_codes_str += f'    {{\n'
            stored_codes_str += f'        "name": "{safe_name}",\n'
            stored_codes_str += f'        "code_b64": (\n        "{formatted_b64}"\n        )\n'
            stored_codes_str += f'    }},\n'
        stored_codes_str += "]\n"
        
        current_time_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        container_prefix = f"CONT_{current_time_str}"
        
        target_title = props.container_title.strip()
        target_category = props.container_category.strip()
        
        # カスタムリンク1設定の検証
        c_link_name = props.custom_link_name.strip()
        c_link_url = props.custom_link_url.strip()

        template = f"""# ===================================================================
# 1. bl_info
# ===================================================================
bl_info = {{
    "name": "{target_title}",
    "author": "Generated by Builder",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > {target_category}",
    "description": "複数のアドオンコードを格納・展開するコンテナ (Blender 5以降専用)",
    "category": "3D View",
}}

# ===================================================================
# 2. import
# ===================================================================
import bpy
import base64
import webbrowser
from bpy.types import Operator, Panel

# ===================================================================
# 3. 共通定義
# ===================================================================
ADDON_TITLE = "{target_title}"
PREFIX_VAL = "{container_prefix}"
ADDON_CATEGORY = "{target_category}"
PREFIX = PREFIX_VAL.lower()

# ===================================================================
# 4. リンク定義
# ===================================================================
LINK_NAME_1 = "{c_link_name}"
LINK_URL_1 = "{c_link_url}"

LINK_NAME_2 = "{LINK_NAME_2}"
LINK_URL_2 = "{LINK_URL_2}"

LINK_NAME_3 = "{LINK_NAME_3}"
LINK_URL_3 = "{LINK_URL_3}"

LINK_NAME_4 = "{LINK_NAME_4}"
LINK_URL_4 = "{LINK_URL_4}"

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================================================
# 5. 共通クラス
# ===================================================================
class {container_prefix}_OT_OpenUrl(Operator):
    bl_idname = f"{{PREFIX}}.open_url"
    bl_label = "リンクを開く"
    
    url: bpy.props.StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {{'FINISHED'}}

# ===================================================================
# 6. アドオン固有機能
# ===================================================================
{stored_codes_str}

class {container_prefix}_OT_OpenCode(Operator):
    bl_idname = f"{{PREFIX}}.open_stored_code"
    bl_label = "テキストエディタで開く"
    
    code_index: bpy.props.IntProperty()
    
    def execute(self, context):
        item = STORED_CODES[self.code_index]
        text_name = item["name"] + ".py"
        
        code_text = base64.b64decode(item["code_b64"]).decode('utf-8')
        
        if text_name in bpy.data.texts:
            t = bpy.data.texts[text_name]
            t.clear()
            t.write(code_text)
        else:
            t = bpy.data.texts.new(text_name)
            t.use_fake_user = True
            t.write(code_text)
            
        editor_found = False
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = t
                    editor_found = True
                    break
            if editor_found:
                break
                
        if not editor_found:
            self.report({{'INFO'}}, f"'{{text_name}}' を生成しました。Text Editorを開いてください。")
        return {{'FINISHED'}}

class {container_prefix}_OT_RemoveContainer(Operator):
    bl_idname = f"{{PREFIX}}.remove_container"
    bl_label = "Unregister Container"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {{'FINISHED'}}

class {container_prefix}_PT_ContainerPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{container_prefix}_PT_container_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if not STORED_CODES:
            box.label(text="コードがありません")
        else:
            for i, item in enumerate(STORED_CODES):
                row = box.row(align=True)
                
                # 左側アドオン情報(ラベル)が最大幅を確保し、横に伸びるよう配置
                row.label(text=str(i + 1) + ": " + item["name"], icon='FILE_SCRIPT')
                
                # 右揃えサブ行を設置し、「展開」ボタンを常に最小サイズで右寄せ維持します
                op_row = row.row(align=True)
                op_row.alignment = 'RIGHT'
                
                op = op_row.operator(f"{{PREFIX}}.open_stored_code", text="展開", icon='TEXT')
                op.code_index = i

class {container_prefix}_PT_ContainerLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{container_prefix}_PT_container_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        # 1番はユーザー指定のカスタムリンク(入力された場合のみ描画)
        if LINK_NAME_1 and LINK_URL_1:
            op1 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_1, icon='URL')
            op1.url = LINK_URL_1
        
        if LINK_NAME_2:
            op2 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_2, icon='URL')
            op2.url = LINK_URL_2
        
        if LINK_NAME_3:
            op3 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_3, icon='URL')
            op3.url = LINK_URL_3
            
        if LINK_NAME_4:
            op4 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_4, icon='URL')
            op4.url = LINK_URL_4

class {container_prefix}_PT_ContainerRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{container_prefix}_PT_container_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {{'DEFAULT_CLOSED'}}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このコンテナアドオンを消去します", icon='ERROR')
        box.operator(f"{{PREFIX}}.remove_container", icon='CANCEL')

# ===================================================================
# 7. register / unregister
# ===================================================================
classes = (
    {container_prefix}_OT_OpenUrl,
    {container_prefix}_OT_OpenCode,
    {container_prefix}_OT_RemoveContainer,
    {container_prefix}_PT_ContainerPanel,
    {container_prefix}_PT_ContainerLinksPanel,
    {container_prefix}_PT_ContainerRemovePanel
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
"""
        out_name = f"{target_title}.py".replace(" ", "_").replace(" ", "_")
        if out_name in bpy.data.texts:
            out_text = bpy.data.texts[out_name]
            out_text.clear()
        else:
            out_text = bpy.data.texts.new(out_name)
            
        out_text.write(template)
        
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    
        self.report({'INFO'}, f"コンテナコード '{out_name}' を生成しました")
        return {'FINISHED'}

# =========================================================
# UI パネル (UIListを含む)
# =========================================================
class ZIONAD_UL_BuilderList(UIList):
    bl_idname = f"{PREFIX_VAL}_UL_builder_list"
    
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index=0, flt_flag=0):
        layout.label(text=f"{index + 1}: {item.name}", icon='FILE_SCRIPT')

class ZIONAD_PT_BuilderPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_builder_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = get_props(context)
        if not props: return
        
        box = layout.box()
        box.label(text="1. 取り込み操作:")
        box.operator(ZIONAD_OT_BuilderAddFile.bl_idname, icon='FILEBROWSER')
        box.operator(ZIONAD_OT_BuilderAddClipboard.bl_idname, icon='PASTEDOWN')
        
        layout.separator()
        layout.label(text="2. コード一覧(ソート・順序変更):")
        
        row = layout.row()
        row.template_list(ZIONAD_UL_BuilderList.bl_idname, "", props, "items", props, "active_index", rows=5)
        
        col = row.column(align=True)
        col.operator(ZIONAD_OT_BuilderRemove.bl_idname, text="", icon='REMOVE')
        col.separator()
        op_up = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_UP')
        op_up.direction = 'UP'
        op_down = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_DOWN')
        op_down.direction = 'DOWN'
        
        layout.operator(ZIONAD_OT_BuilderSortItems.bl_idname, icon='SORTALPHA')
        
        # 3. コンテナ設定 (カスタムリンク設定入力フィールドを含む)
        layout.separator()
        layout.label(text="3. コンテナ設定:")
        box_cfg = layout.box()
        box_cfg.prop(props, "container_title")
        box_cfg.prop(props, "container_category")
        
        box_cfg.separator()
        box_cfg.label(text="追加カスタムリンク (任意項目):", icon='URL')
        box_cfg.prop(props, "custom_link_name", text="リンク1 タイトル")
        box_cfg.prop(props, "custom_link_url", text="リンク1 URL")
        
        # 4. アドオンの作成
        layout.separator()
        layout.label(text="4. アドオンの作成:")
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(ZIONAD_OT_BuilderGenerate.bl_idname, icon='FILE_TICK')

class ZIONAD_PT_BuilderLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_builder_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        # シフトした2番〜4番への参照
        op2 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_2, icon='URL')
        op2.url = LINK_URL_2
        
        op3 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_3, icon='URL')
        op3.url = LINK_URL_3

        op4 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_4, icon='URL')
        op4.url = LINK_URL_4

class ZIONAD_PT_BuilderRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_builder_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このビルダーアドオンを消去します", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveBuilder.bl_idname, icon='CANCEL')

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_BuilderItem,
    ZIONAD_BuilderProps,
    ZIONAD_OT_BuilderAddClipboard,
    ZIONAD_OT_BuilderAddFile,
    ZIONAD_OT_BuilderRemove,
    ZIONAD_OT_BuilderMoveItem,
    ZIONAD_OT_BuilderSortItems,
    ZIONAD_OT_OpenUrl,
    ZIONAD_OT_RemoveBuilder,
    ZIONAD_OT_BuilderGenerate,
    ZIONAD_UL_BuilderList,
    ZIONAD_PT_BuilderPanel,
    ZIONAD_PT_BuilderLinksPanel,
    ZIONAD_PT_BuilderRemovePanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    prop_name = f"{PREFIX_VAL.lower()}_props"
    setattr(bpy.types.Scene, prop_name, PointerProperty(type=ZIONAD_BuilderProps))

def unregister():
    prop_name = f"{PREFIX_VAL.lower()}_props"
    if hasattr(bpy.types.Scene, prop_name):
        delattr(bpy.types.Scene, prop_name)
        
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
展開 表示欄は サイドバー表示を長くしても 最小大きさ維持して

# ===================================================================
# 【リンク定義】 
# ===================================================================
LINK_NAME_1 = "コンテナ builder"
LINK_URL_1 = "<https://note.com/zionadmillion/n/n2158226ce616>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

このようにして
そして いまリンク1を 2にして
残りも ずらして

リンク1 タイトルと URLを
コンテナ作成時に 入力できるようにして

入力がなかったら リンク1タイトルとURLなしにする

import bpy
import re
import os
import base64
import webbrowser
import datetime
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty
from bpy_extras.io_utils import ImportHelper

# ===================================================================
# 【定義】
# ===================================================================
ADDON_TITLE = "コンテナ builder"
PREFIX_VAL = "BUILDER_1001"
ADDON_CATEGORY = "コンテナ builder"

# ===================================================================
# 【名前初期値設定】
# ===================================================================
# 生成されるコンテナ名とサイドバー表示名の初期パターンを設定します。
# 「{datetime}」と記述された部分は、自動的に「YYYYMMDDHHMM」形式の現在日時に置換されます。
DEFAULT_NAME_TITLE = "コンテナ {datetime}"
DEFAULT_NAME_CATEGORY = "コンテナ {datetime}"

# ===================================================================
# 【リンク定義】 
# ===================================================================
LINK_NAME = "コンテナ builder"
LINK_URL_1 = "<https://note.com/zionadmillion/n/n2158226ce616>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

# =========================================================
# コンテナ生成ビルダー アドオン (Blender 5以降専用)
# =========================================================
bl_info = {
    "name": "コンテナ builder",
    "author": "AI",
    "version": (2, 8, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > コンテナ builder",
    "description": "エラーが起きないようにBase64変換してコンテナアドオンを生成します",
    "category": "3D View",
}

# =========================================================
# プロパティ取得ヘルパー
# =========================================================
def get_props(context):
    prop_name = f"{PREFIX_VAL.lower()}_props"
    return getattr(context.scene, prop_name, None)

# =========================================================
# カスタムゲッター/セッター (動的な日時デフォルト値用)
# =========================================================
def get_dynamic_default_title(self):
    if not self.get("container_title_stored", ""):
        now_str = datetime.datetime.now().strftime("%Y%m%d%H%M")
        return DEFAULT_NAME_TITLE.replace("{datetime}", now_str)
    return self.get("container_title_stored", "")

def set_dynamic_default_title(self, value):
    self["container_title_stored"] = value

def get_dynamic_default_category(self):
    if not self.get("container_category_stored", ""):
        now_str = datetime.datetime.now().strftime("%Y%m%d%H%M")
        return DEFAULT_NAME_CATEGORY.replace("{datetime}", now_str)
    return self.get("container_category_stored", "")

def set_dynamic_default_category(self, value):
    self["container_category_stored"] = value

# =========================================================
# データ構造
# =========================================================
class ZIONAD_BuilderItem(PropertyGroup):
    name: StringProperty()
    text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)

class ZIONAD_BuilderProps(PropertyGroup):
    items: CollectionProperty(type=ZIONAD_BuilderItem)
    active_index: IntProperty(default=0)
    
    # 動的な初期値を持つプロパティ
    container_title: StringProperty(
        name="コンテナ名",
        description="生成するアドオンの正式名称 (ADDON_TITLE)",
        get=get_dynamic_default_title,
        set=set_dynamic_default_title
    )
    container_category: StringProperty(
        name="サイドバー表示名",
        description="3Dビューのサイドバーに表示されるタブ名 (ADDON_CATEGORY)",
        get=get_dynamic_default_category,
        set=set_dynamic_default_category
    )

# =========================================================
# オペレーター: 追加・削除・リンク・アンインストール
# =========================================================
class ZIONAD_OT_BuilderAddClipboard(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.add_clipboard"
    bl_label = "クリップボードから取り込む"
    
    def execute(self, context):
        text = context.window_manager.clipboard
        if not text or len(text.strip()) == 0:
            self.report({'WARNING'}, "クリップボードが空です。")
            return {'CANCELLED'}
            
        props = get_props(context)
        if not props: return {'CANCELLED'}
            
        match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
        base_name = match.group(1) if match else f"Clipboard_Addon_{len(props.items)+1}"
        
        t = bpy.data.texts.new(name=f"{base_name}.py")
        t.write(text)
        
        item = props.items.add()
        item.name = base_name
        item.text_ptr = t
        props.active_index = len(props.items) - 1
        
        self.report({'INFO'}, f"{base_name} を追加しました")
        return {'FINISHED'}

class ZIONAD_OT_BuilderAddFile(Operator, ImportHelper):
    bl_idname = f"{PREFIX_VAL.lower()}.add_file"
    bl_label = "フォルダから.pyを選ぶ(複数可)"
    bl_description = "複数の.pyファイルを選択して一度に取り込みます (Shift/Ctrlキーで複数選択)"
    
    filename_ext = ".py"
    filter_glob: StringProperty(default="*.py", options={'HIDDEN'})
    
    files: CollectionProperty(
        name="File Path",
        type=bpy.types.OperatorFileListElement,
    )
    directory: StringProperty(
        subtype='DIR_PATH',
    )
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        added_count = 0
        
        if self.files:
            for file_elem in self.files:
                name = file_elem.name
                if not name:
                    continue
                    
                filepath = os.path.join(self.directory, name)
                
                try:
                    with open(filepath, 'r', encoding='utf-8') as f:
                        text = f.read()
                except Exception as e:
                    self.report({'ERROR'}, f"ファイルの読み込み失敗: {name} - {e}")
                    continue
                    
                t = bpy.data.texts.new(name=name)
                t.write(text)
                
                item = props.items.add()
                item.name = name.replace('.py', '')
                item.text_ptr = t
                added_count += 1
                
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"{added_count}個のファイルを追加しました")
        else:
            self.report({'WARNING'}, "ファイルが追加されませんでした")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderRemove(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_item"
    bl_label = "選択した項目を削除"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        item = props.items[idx]
        if item.text_ptr:
            bpy.data.texts.remove(item.text_ptr)
            
        props.items.remove(idx)
        props.active_index = max(0, min(idx, len(props.items) - 1))
        return {'FINISHED'}

class ZIONAD_OT_BuilderMoveItem(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.move_item"
    bl_label = "順番を移動"
    bl_description = "選択したアドオンの順番を上下に移動します"
    
    direction: StringProperty()
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        new_idx = idx - 1 if self.direction == 'UP' else idx + 1
        
        if 0 <= new_idx < len(props.items):
            props.items.move(idx, new_idx)
            props.active_index = new_idx
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderSortItems(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.sort_items"
    bl_label = "名前順にソート"
    bl_description = "追加されたアドオンをアルファベット順に並べ替えます"
    
    def execute(self, context):
        props = get_props(context)
        if not props or len(props.items) < 2: 
            return {'CANCELLED'}
            
        items_data = [(item.name, item.text_ptr) for item in props.items]
        items_data.sort(key=lambda x: x[0].lower())
        
        props.items.clear()
        for name, txt in items_data:
            new_item = props.items.add()
            new_item.name = name
            new_item.text_ptr = txt
            
        props.active_index = 0
        self.report({'INFO'}, "アドオンを名前順にソートしました")
        return {'FINISHED'}

class ZIONAD_OT_OpenUrl(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.open_url"
    bl_label = "リンクを開く"
    
    url: StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class ZIONAD_OT_RemoveBuilder(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_builder"
    bl_label = "Unregister Builder"
    bl_description = "このビルダーアドオンを無効化し、UIから消去します"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# オペレーター: コンテナコードの生成と出力 (メイン処理)
# =========================================================
class ZIONAD_OT_BuilderGenerate(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.generate"
    bl_label = "コンテナコードを出力"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items:
            self.report({'WARNING'}, "コードが一つも追加されていません。")
            return {'CANCELLED'}
            
        stored_codes_str = "STORED_CODES = [\n"
        for item in props.items:
            if not item.text_ptr: continue
            raw_code = item.text_ptr.as_string()
            safe_name = item.name
            
            encoded_bytes = base64.b64encode(raw_code.encode('utf-8'))
            encoded_str = encoded_bytes.decode('utf-8')
            chunks = [encoded_str[i:i+80] for i in range(0, len(encoded_str), 80)]
            formatted_b64 = '"\n        "'.join(chunks)
            
            stored_codes_str += f'    {{\n'
            stored_codes_str += f'        "name": "{safe_name}",\n'
            stored_codes_str += f'        "code_b64": (\n        "{formatted_b64}"\n        )\n'
            stored_codes_str += f'    }},\n'
        stored_codes_str += "]\n"
        
        current_time_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        container_prefix = f"CONT_{current_time_str}"
        
        target_title = props.container_title.strip()
        target_category = props.container_category.strip()

        template = f"""# ===================================================================
# 1. bl_info
# ===================================================================
bl_info = {{
    "name": "{target_title}",
    "author": "Generated by Builder",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > {target_category}",
    "description": "複数のアドオンコードを格納・展開するコンテナ (Blender 5以降専用)",
    "category": "3D View",
}}

# ===================================================================
# 2. import
# ===================================================================
import bpy
import base64
import webbrowser
from bpy.types import Operator, Panel

# ===================================================================
# 3. 共通定義
# ===================================================================
ADDON_TITLE = "{target_title}"
PREFIX_VAL = "{container_prefix}"
ADDON_CATEGORY = "{target_category}"
PREFIX = PREFIX_VAL.lower()

# ===================================================================
# 4. リンク定義
# ===================================================================
LINK_NAME_1 = "{LINK_NAME}"
LINK_URL_1 = "{LINK_URL_1}"

LINK_NAME_2 = "{LINK_NAME_2}"
LINK_URL_2 = "{LINK_URL_2}"

LINK_NAME_3 = "{LINK_NAME_3}"
LINK_URL_3 = "{LINK_URL_3}"

LINK_NAME_4 = ""
LINK_URL_4 = ""

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================================================
# 5. 共通クラス
# ===================================================================
class {container_prefix}_OT_OpenUrl(Operator):
    bl_idname = f"{{PREFIX}}.open_url"
    bl_label = "リンクを開く"
    
    url: bpy.props.StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {{'FINISHED'}}

# ===================================================================
# 6. アドオン固有機能
# ===================================================================
{stored_codes_str}

class {container_prefix}_OT_OpenCode(Operator):
    bl_idname = f"{{PREFIX}}.open_stored_code"
    bl_label = "テキストエディタで開く"
    
    code_index: bpy.props.IntProperty()
    
    def execute(self, context):
        item = STORED_CODES[self.code_index]
        text_name = item["name"] + ".py"
        
        code_text = base64.b64decode(item["code_b64"]).decode('utf-8')
        
        if text_name in bpy.data.texts:
            t = bpy.data.texts[text_name]
            t.clear()
            t.write(code_text)
        else:
            t = bpy.data.texts.new(text_name)
            t.use_fake_user = True
            t.write(code_text)
            
        editor_found = False
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = t
                    editor_found = True
                    break
            if editor_found:
                break
                
        if not editor_found:
            self.report({{'INFO'}}, f"'{{text_name}}' を生成しました。Text Editorを開いてください。")
        return {{'FINISHED'}}

class {container_prefix}_OT_RemoveContainer(Operator):
    bl_idname = f"{{PREFIX}}.remove_container"
    bl_label = "Unregister Container"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {{'FINISHED'}}

class {container_prefix}_PT_ContainerPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{container_prefix}_PT_container_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if not STORED_CODES:
            box.label(text="コードがありません")
        else:
            for i, item in enumerate(STORED_CODES):
                row = box.row(align=True)
                
                # split構造を使用して、右側の「展開」ボタン表示域をコンパクトに保ちます
                split = row.split(factor=0.82, align=True)
                split.label(text=str(i + 1) + ": " + item["name"], icon='FILE_SCRIPT')
                
                op = split.operator(f"{{PREFIX}}.open_stored_code", text="展開", icon='TEXT')
                op.code_index = i

class {container_prefix}_PT_ContainerLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{container_prefix}_PT_container_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        if LINK_NAME_1:
            op = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_1, icon='URL')
            op.url = LINK_URL_1
        
        if LINK_NAME_2:
            op2 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_2, icon='URL')
            op2.url = LINK_URL_2
        
        if LINK_NAME_3:
            op3 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_3, icon='URL')
            op3.url = LINK_URL_3

class {container_prefix}_PT_ContainerRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{container_prefix}_PT_container_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {{'DEFAULT_CLOSED'}}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このコンテナアドオンを消去します", icon='ERROR')
        box.operator(f"{{PREFIX}}.remove_container", icon='CANCEL')

# ===================================================================
# 7. register / unregister
# ===================================================================
classes = (
    {container_prefix}_OT_OpenUrl,
    {container_prefix}_OT_OpenCode,
    {container_prefix}_OT_RemoveContainer,
    {container_prefix}_PT_ContainerPanel,
    {container_prefix}_PT_ContainerLinksPanel,
    {container_prefix}_PT_ContainerRemovePanel
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
"""
        out_name = f"{target_title}.py".replace(" ", "_").replace(" ", "_")
        if out_name in bpy.data.texts:
            out_text = bpy.data.texts[out_name]
            out_text.clear()
        else:
            out_text = bpy.data.texts.new(out_name)
            
        out_text.write(template)
        
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    
        self.report({'INFO'}, f"コンテナコード '{out_name}' を生成しました")
        return {'FINISHED'}

# =========================================================
# UI パネル (UIListを含む)
# =========================================================
class ZIONAD_UL_BuilderList(UIList):
    bl_idname = f"{PREFIX_VAL}_UL_builder_list"
    
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index=0, flt_flag=0):
        layout.label(text=f"{index + 1}: {item.name}", icon='FILE_SCRIPT')

class ZIONAD_PT_BuilderPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_builder_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = get_props(context)
        if not props: return
        
        box = layout.box()
        box.label(text="1. 取り込み操作:")
        box.operator(ZIONAD_OT_BuilderAddFile.bl_idname, icon='FILEBROWSER')
        box.operator(ZIONAD_OT_BuilderAddClipboard.bl_idname, icon='PASTEDOWN')
        
        layout.separator()
        layout.label(text="2. コード一覧(ソート・順序変更):")
        
        row = layout.row()
        row.template_list(ZIONAD_UL_BuilderList.bl_idname, "", props, "items", props, "active_index", rows=5)
        
        col = row.column(align=True)
        col.operator(ZIONAD_OT_BuilderRemove.bl_idname, text="", icon='REMOVE')
        col.separator()
        op_up = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_UP')
        op_up.direction = 'UP'
        op_down = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_DOWN')
        op_down.direction = 'DOWN'
        
        layout.operator(ZIONAD_OT_BuilderSortItems.bl_idname, icon='SORTALPHA')
        
        # 3. コンテナ設定
        layout.separator()
        layout.label(text="3. コンテナ設定:")
        box_cfg = layout.box()
        box_cfg.prop(props, "container_title")
        box_cfg.prop(props, "container_category")
        
        # 4. アドオンの作成
        layout.separator()
        layout.label(text="4. アドオンの作成:")
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(ZIONAD_OT_BuilderGenerate.bl_idname, icon='FILE_TICK')

class ZIONAD_PT_BuilderLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_builder_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        op = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME, icon='URL')
        op.url = LINK_URL_1
        
        op2 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_2, icon='URL')
        op2.url = LINK_URL_2
        
        op3 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_3, icon='URL')
        op3.url = LINK_URL_3

class ZIONAD_PT_BuilderRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_builder_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このビルダーアドオンを消去します", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveBuilder.bl_idname, icon='CANCEL')

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_BuilderItem,
    ZIONAD_BuilderProps,
    ZIONAD_OT_BuilderAddClipboard,
    ZIONAD_OT_BuilderAddFile,
    ZIONAD_OT_BuilderRemove,
    ZIONAD_OT_BuilderMoveItem,
    ZIONAD_OT_BuilderSortItems,
    ZIONAD_OT_OpenUrl,
    ZIONAD_OT_RemoveBuilder,
    ZIONAD_OT_BuilderGenerate,
    ZIONAD_UL_BuilderList,
    ZIONAD_PT_BuilderPanel,
    ZIONAD_PT_BuilderLinksPanel,
    ZIONAD_PT_BuilderRemovePanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    prop_name = f"{PREFIX_VAL.lower()}_props"
    setattr(bpy.types.Scene, prop_name, PointerProperty(type=ZIONAD_BuilderProps))

def unregister():
    prop_name = f"{PREFIX_VAL.lower()}_props"
    if hasattr(bpy.types.Scene, prop_name):
        delattr(bpy.types.Scene, prop_name)
        
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
作成したコンテナのUI表示で サイドバーを長くしても 
「展開」の表示域は 最小横幅にする 
左に表示される アドオンタイトル?情報を 表示する領域を大きくしたいから

builder で 

コンテナ名 初期値 
コンテナ  +年月日時分

サイドバー表示名 初期値
コンテナ +年月日時分

この名前設定 変更できるように
リンク定義の 上行に置いて

import bpy
import re
import os
import base64
import webbrowser
import datetime
from bpy.types import Operator, Panel, PropertyGroup, UIList
from bpy.props import StringProperty, PointerProperty, CollectionProperty, IntProperty
from bpy_extras.io_utils import ImportHelper

# ===================================================================
# 【定義】
# ===================================================================
ADDON_TITLE = "コンテナ builder"
PREFIX_VAL = "BUILDER_1001"
ADDON_CATEGORY = "コンテナ builder"

# ===================================================================
# 【リンク定義】 
# ===================================================================
LINK_NAME = "コンテナ builder"
LINK_URL_1 = "<https://note.com/zionadmillion/n/n2158226ce616>"
LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

# =========================================================
# コンテナ生成ビルダー アドオン (Blender 5以降専用)
# =========================================================
bl_info = {
    "name": "コンテナ builder",
    "author": "AI",
    "version": (2, 6, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > コンテナ builder",
    "description": "エラーが起きないようにBase64変換してコンテナアドオンを生成します",
    "category": "3D View",
}

# =========================================================
# プロパティ取得ヘルパー
# =========================================================
def get_props(context):
    prop_name = f"{PREFIX_VAL.lower()}_props"
    return getattr(context.scene, prop_name, None)

# =========================================================
# データ構造
# =========================================================
class ZIONAD_BuilderItem(PropertyGroup):
    name: StringProperty()
    text_ptr: PointerProperty(name="Text Data", type=bpy.types.Text)

class ZIONAD_BuilderProps(PropertyGroup):
    items: CollectionProperty(type=ZIONAD_BuilderItem)
    active_index: IntProperty(default=0)
    
    # 新設: 生成するコンテナのカスタマイズ用プロパティ
    container_title: StringProperty(
        name="コンテナ名",
        description="生成するアドオンの表示名 (ADDON_TITLE)",
        default="マイコンテナ"
    )
    container_category: StringProperty(
        name="サイドバー表示名",
        description="3Dビューのサイドバーに表示されるタブ名 (ADDON_CATEGORY)",
        default="マイコンテナ"
    )

# =========================================================
# オペレーター: 追加・削除・リンク・アンインストール
# =========================================================
class ZIONAD_OT_BuilderAddClipboard(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.add_clipboard"
    bl_label = "クリップボードから取り込む"
    
    def execute(self, context):
        text = context.window_manager.clipboard
        if not text or len(text.strip()) == 0:
            self.report({'WARNING'}, "クリップボードが空です。")
            return {'CANCELLED'}
            
        props = get_props(context)
        if not props: return {'CANCELLED'}
            
        match = re.search(r'["\']name["\']\s*:\s*["\']([^"\']+)["\']', text)
        base_name = match.group(1) if match else f"Clipboard_Addon_{len(props.items)+1}"
        
        t = bpy.data.texts.new(name=f"{base_name}.py")
        t.write(text)
        
        item = props.items.add()
        item.name = base_name
        item.text_ptr = t
        props.active_index = len(props.items) - 1
        
        self.report({'INFO'}, f"{base_name} を追加しました")
        return {'FINISHED'}

class ZIONAD_OT_BuilderAddFile(Operator, ImportHelper):
    bl_idname = f"{PREFIX_VAL.lower()}.add_file"
    bl_label = "フォルダから.pyを選ぶ(複数可)"
    bl_description = "複数の.pyファイルを選択して一度に取り込みます (Shift/Ctrlキーで複数選択)"
    
    filename_ext = ".py"
    filter_glob: StringProperty(default="*.py", options={'HIDDEN'})
    
    files: CollectionProperty(
        name="File Path",
        type=bpy.types.OperatorFileListElement,
    )
    directory: StringProperty(
        subtype='DIR_PATH',
    )
    
    def execute(self, context):
        props = get_props(context)
        if not props: return {'CANCELLED'}
        
        added_count = 0
        
        if self.files:
            for file_elem in self.files:
                name = file_elem.name
                if not name:
                    continue
                    
                filepath = os.path.join(self.directory, name)
                
                try:
                    with open(filepath, 'r', encoding='utf-8') as f:
                        text = f.read()
                except Exception as e:
                    self.report({'ERROR'}, f"ファイルの読み込み失敗: {name} - {e}")
                    continue
                    
                t = bpy.data.texts.new(name=name)
                t.write(text)
                
                item = props.items.add()
                item.name = name.replace('.py', '')
                item.text_ptr = t
                added_count += 1
                
        if added_count > 0:
            props.active_index = len(props.items) - 1
            self.report({'INFO'}, f"{added_count}個のファイルを追加しました")
        else:
            self.report({'WARNING'}, "ファイルが追加されませんでした")
            return {'CANCELLED'}
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderRemove(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_item"
    bl_label = "選択した項目を削除"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        item = props.items[idx]
        if item.text_ptr:
            bpy.data.texts.remove(item.text_ptr)
            
        props.items.remove(idx)
        props.active_index = max(0, min(idx, len(props.items) - 1))
        return {'FINISHED'}

class ZIONAD_OT_BuilderMoveItem(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.move_item"
    bl_label = "順番を移動"
    bl_description = "選択したアドオンの順番を上下に移動します"
    
    direction: StringProperty()
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items: return {'CANCELLED'}
        
        idx = props.active_index
        new_idx = idx - 1 if self.direction == 'UP' else idx + 1
        
        if 0 <= new_idx < len(props.items):
            props.items.move(idx, new_idx)
            props.active_index = new_idx
            
        return {'FINISHED'}

class ZIONAD_OT_BuilderSortItems(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.sort_items"
    bl_label = "名前順にソート"
    bl_description = "追加されたアドオンをアルファベット順に並べ替えます"
    
    def execute(self, context):
        props = get_props(context)
        if not props or len(props.items) < 2: 
            return {'CANCELLED'}
            
        items_data = [(item.name, item.text_ptr) for item in props.items]
        items_data.sort(key=lambda x: x[0].lower())
        
        props.items.clear()
        for name, txt in items_data:
            new_item = props.items.add()
            new_item.name = name
            new_item.text_ptr = txt
            
        props.active_index = 0
        self.report({'INFO'}, "アドオンを名前順にソートしました")
        return {'FINISHED'}

class ZIONAD_OT_OpenUrl(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.open_url"
    bl_label = "リンクを開く"
    
    url: StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {'FINISHED'}

class ZIONAD_OT_RemoveBuilder(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.remove_builder"
    bl_label = "Unregister Builder"
    bl_description = "このビルダーアドオンを無効化し、UIから消去します"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {'FINISHED'}

# =========================================================
# オペレーター: コンテナコードの生成と出力 (メイン処理)
# =========================================================
class ZIONAD_OT_BuilderGenerate(Operator):
    bl_idname = f"{PREFIX_VAL.lower()}.generate"
    bl_label = "コンテナコードを出力"
    
    def execute(self, context):
        props = get_props(context)
        if not props or not props.items:
            self.report({'WARNING'}, "コードが一つも追加されていません。")
            return {'CANCELLED'}
            
        stored_codes_str = "STORED_CODES = [\n"
        for item in props.items:
            if not item.text_ptr: continue
            raw_code = item.text_ptr.as_string()
            safe_name = item.name
            
            encoded_bytes = base64.b64encode(raw_code.encode('utf-8'))
            encoded_str = encoded_bytes.decode('utf-8')
            chunks = [encoded_str[i:i+80] for i in range(0, len(encoded_str), 80)]
            formatted_b64 = '"\n        "'.join(chunks)
            
            stored_codes_str += f'    {{\n'
            stored_codes_str += f'        "name": "{safe_name}",\n'
            stored_codes_str += f'        "code_b64": (\n        "{formatted_b64}"\n        )\n'
            stored_codes_str += f'    }},\n'
        stored_codes_str += "]\n"
        
        current_time_str = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        container_prefix = f"CONT_{current_time_str}"
        
        # ユーザーが指定した値を適用
        target_title = props.container_title.strip() if props.container_title.strip() else f"Container_{current_time_str}"
        target_category = props.container_category.strip() if props.container_category.strip() else target_title

        # 指定フォーマット(共通構成)に厳密に沿ったテンプレート
        template = f"""# ===================================================================
# 1. bl_info
# ===================================================================
bl_info = {{
    "name": "{target_title}",
    "author": "Generated by Builder",
    "version": (1, 0, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > {target_category}",
    "description": "複数のアドオンコードを格納・展開するコンテナ (Blender 5以降専用)",
    "category": "3D View",
}}

# ===================================================================
# 2. import
# ===================================================================
import bpy
import base64
import webbrowser
from bpy.types import Operator, Panel

# ===================================================================
# 3. 共通定義
# ===================================================================
ADDON_TITLE = "{target_title}"
PREFIX_VAL = "{container_prefix}"
ADDON_CATEGORY = "{target_category}"
PREFIX = PREFIX_VAL.lower()

# ===================================================================
# 4. リンク定義
# ===================================================================
LINK_NAME_1 = "{LINK_NAME}"
LINK_URL_1 = "{LINK_URL_1}"

LINK_NAME_2 = "{LINK_NAME_2}"
LINK_URL_2 = "{LINK_URL_2}"

LINK_NAME_3 = "{LINK_NAME_3}"
LINK_URL_3 = "{LINK_URL_3}"

LINK_NAME_4 = ""
LINK_URL_4 = ""

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================================================
# 5. 共通クラス
# ===================================================================
class {container_prefix}_OT_OpenUrl(Operator):
    bl_idname = f"{{PREFIX}}.open_url"
    bl_label = "リンクを開く"
    
    url: bpy.props.StringProperty()
    
    def execute(self, context):
        webbrowser.open(self.url)
        return {{'FINISHED'}}

# ===================================================================
# 6. アドオン固有機能
# ===================================================================
{stored_codes_str}

class {container_prefix}_OT_OpenCode(Operator):
    bl_idname = f"{{PREFIX}}.open_stored_code"
    bl_label = "テキストエディタで開く"
    
    code_index: bpy.props.IntProperty()
    
    def execute(self, context):
        item = STORED_CODES[self.code_index]
        text_name = item["name"] + ".py"
        
        code_text = base64.b64decode(item["code_b64"]).decode('utf-8')
        
        if text_name in bpy.data.texts:
            t = bpy.data.texts[text_name]
            t.clear()
            t.write(code_text)
        else:
            t = bpy.data.texts.new(text_name)
            t.use_fake_user = True
            t.write(code_text)
            
        editor_found = False
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = t
                    editor_found = True
                    break
            if editor_found:
                break
                
        if not editor_found:
            self.report({{'INFO'}}, f"'{{text_name}}' を生成しました。Text Editorを開いてください。")
        return {{'FINISHED'}}

class {container_prefix}_OT_RemoveContainer(Operator):
    bl_idname = f"{{PREFIX}}.remove_container"
    bl_label = "Unregister Container"
    
    def execute(self, context):
        bpy.app.timers.register(unregister, first_interval=0.01)
        return {{'FINISHED'}}

class {container_prefix}_PT_ContainerPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{container_prefix}_PT_container_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        if not STORED_CODES:
            box.label(text="コードがありません")
        else:
            for i, item in enumerate(STORED_CODES):
                row = box.row(align=True)
                row.label(text=str(i + 1) + ": " + item["name"], icon='FILE_SCRIPT')
                op = row.operator(f"{{PREFIX}}.open_stored_code", text="展開", icon='TEXT')
                op.code_index = i

class {container_prefix}_PT_ContainerLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{container_prefix}_PT_container_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        if LINK_NAME_1:
            op = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_1, icon='URL')
            op.url = LINK_URL_1
        
        if LINK_NAME_2:
            op2 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_2, icon='URL')
            op2.url = LINK_URL_2
        
        if LINK_NAME_3:
            op3 = box.operator(f"{{PREFIX}}.open_url", text=LINK_NAME_3, icon='URL')
            op3.url = LINK_URL_3

class {container_prefix}_PT_ContainerRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{container_prefix}_PT_container_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {{'DEFAULT_CLOSED'}}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このコンテナアドオンを消去します", icon='ERROR')
        box.operator(f"{{PREFIX}}.remove_container", icon='CANCEL')

# ===================================================================
# 7. register / unregister
# ===================================================================
classes = (
    {container_prefix}_OT_OpenUrl,
    {container_prefix}_OT_OpenCode,
    {container_prefix}_OT_RemoveContainer,
    {container_prefix}_PT_ContainerPanel,
    {container_prefix}_PT_ContainerLinksPanel,
    {container_prefix}_PT_ContainerRemovePanel
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()
"""
        out_name = f"{target_title}.py".replace(" ", "_").replace(" ", "_")
        if out_name in bpy.data.texts:
            out_text = bpy.data.texts[out_name]
            out_text.clear()
        else:
            out_text = bpy.data.texts.new(out_name)
            
        out_text.write(template)
        
        # テキストエディターを自動でアクティブ化
        for win in context.window_manager.windows:
            for area in win.screen.areas:
                if area.type == 'TEXT_EDITOR':
                    area.spaces.active.text = out_text
                    
        self.report({'INFO'}, f"コンテナコード '{out_name}' を生成しました")
        return {'FINISHED'}

# =========================================================
# UI パネル (UIListを含む)
# =========================================================
class ZIONAD_UL_BuilderList(UIList):
    bl_idname = f"{PREFIX_VAL}_UL_builder_list"
    
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index=0, flt_flag=0):
        layout.label(text=f"{index + 1}: {item.name}", icon='FILE_SCRIPT')

class ZIONAD_PT_BuilderPanel(Panel):
    bl_label = ADDON_TITLE
    bl_idname = f"{PREFIX_VAL}_PT_builder_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 1

    def draw(self, context):
        layout = self.layout
        props = get_props(context)
        if not props: return
        
        box = layout.box()
        box.label(text="1. 取り込み操作:")
        box.operator(ZIONAD_OT_BuilderAddFile.bl_idname, icon='FILEBROWSER')
        box.operator(ZIONAD_OT_BuilderAddClipboard.bl_idname, icon='PASTEDOWN')
        
        layout.separator()
        layout.label(text="2. コード一覧(ソート・順序変更):")
        
        row = layout.row()
        row.template_list(ZIONAD_UL_BuilderList.bl_idname, "", props, "items", props, "active_index", rows=5)
        
        col = row.column(align=True)
        col.operator(ZIONAD_OT_BuilderRemove.bl_idname, text="", icon='REMOVE')
        col.separator()
        op_up = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_UP')
        op_up.direction = 'UP'
        op_down = col.operator(ZIONAD_OT_BuilderMoveItem.bl_idname, text="", icon='TRIA_DOWN')
        op_down.direction = 'DOWN'
        
        layout.operator(ZIONAD_OT_BuilderSortItems.bl_idname, icon='SORTALPHA')
        
        # 新設: 生成コンテナの設定変更領域
        layout.separator()
        layout.label(text="3. コンテナ設定:")
        box_cfg = layout.box()
        box_cfg.prop(props, "container_title")
        box_cfg.prop(props, "container_category")
        
        layout.separator()
        layout.label(text="4. アドオンの作成:")
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(ZIONAD_OT_BuilderGenerate.bl_idname, icon='FILE_TICK')

class ZIONAD_PT_BuilderLinksPanel(Panel):
    bl_label = "リンク"
    bl_idname = f"{PREFIX_VAL}_PT_builder_links_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_order = 2

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        
        op = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME, icon='URL')
        op.url = LINK_URL_1
        
        op2 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_2, icon='URL')
        op2.url = LINK_URL_2
        
        op3 = box.operator(ZIONAD_OT_OpenUrl.bl_idname, text=LINK_NAME_3, icon='URL')
        op3.url = LINK_URL_3

class ZIONAD_PT_BuilderRemovePanel(Panel):
    bl_label = "アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_builder_remove_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    bl_options = {'DEFAULT_CLOSED'}
    bl_order = 3

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        box.label(text="このビルダーアドオンを消去します", icon='ERROR')
        box.operator(ZIONAD_OT_RemoveBuilder.bl_idname, icon='CANCEL')

# =========================================================
# 登録・解除
# =========================================================
classes = (
    ZIONAD_BuilderItem,
    ZIONAD_BuilderProps,
    ZIONAD_OT_BuilderAddClipboard,
    ZIONAD_OT_BuilderAddFile,
    ZIONAD_OT_BuilderRemove,
    ZIONAD_OT_BuilderMoveItem,
    ZIONAD_OT_BuilderSortItems,
    ZIONAD_OT_OpenUrl,
    ZIONAD_OT_RemoveBuilder,
    ZIONAD_OT_BuilderGenerate,
    ZIONAD_UL_BuilderList,
    ZIONAD_PT_BuilderPanel,
    ZIONAD_PT_BuilderLinksPanel,
    ZIONAD_PT_BuilderRemovePanel,
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    prop_name = f"{PREFIX_VAL.lower()}_props"
    setattr(bpy.types.Scene, prop_name, PointerProperty(type=ZIONAD_BuilderProps))

def unregister():
    prop_name = f"{PREFIX_VAL.lower()}_props"
    if hasattr(bpy.types.Scene, prop_name):
        delattr(bpy.types.Scene, prop_name)
        
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()