const MaterialComponent = {
  mixins: [BaseMixin],
  props: {
    searchQuery: { type: String, default: '' }
  },
  data() {
    return {
      // Sidebar state
      sidebarOpen: true,
      sidebarState: 'sidebar-open',

      defaultSource: [],
      onlineSources: [],
      sortOrderFolders: 1,
      // Flag to control auto-sorting: true = sort on load, false = manual sort only
      autoSortEnabled: true,
      materialFolders: [],
      materialFolder: {
        folderPath: '',
        originFolderName: '',
        customFolderName: '',
        folderItems: ''
      },
      materialItems: [],
      materialItem: {
        itemPath: '',
        itemName: '',
        itemType: '',
        itemColor: '',
        itemDeltas: [],
        itemSize: '',
        thumbnailPath: ''
      },
      isOnline: false,

      currentUrl: '',
      iframeBlocked: false,
      selectedOnlineName: '',
      selectedOnlineUrl: '',

      folderThumbnailPath: null,
      recentFolderPath: null,
      recentChildFolder: null,
      showShowEditMaterial: true,
      activeFolderPath: null,
      selectedFolderPath: null,
      treeRootPath: null,
      treeRootItems: null,
      _browseFromLeft: false,
      selectedType: null,
      selectedItemType: null,
      actionFolderPath: null,

      selectedMaterials: [], // From SketchUp selection
      selectedItem: null,
      selectedItems: [],
      nameExists: false,
      isSavingName: false, // Lock to prevent multiple save calls

      isDrive: false,
      isSketchupMaterial: false,
      folderEmpty: false,
      showSupport: false,

      resizeObservers: [],
      overlayStyles: [],

      // Deep Search
      deepSearchResults: [],
      isDeepSearching: false,

      // 左侧边栏可调宽度（拖拽分割线）
      sidebarWidth: typeof SIDEBAR_WIDTH_DEFAULT !== 'undefined' ? SIDEBAR_WIDTH_DEFAULT : 230,
      _resizeStartX: 0,
      _resizeStartWidth: 0,
      // 二级目录列可调宽度（与右侧内容区间分割线）
      subfolderColumnWidth: typeof SUBFOLDER_WIDTH_DEFAULT !== 'undefined' ? SUBFOLDER_WIDTH_DEFAULT : 160,
      _resizeSubfolderStartX: 0,
      _resizeSubfolderStartWidth: 0,
      // 二级目录列显示/隐藏（顶部汉堡菜单只控制此项）
      subfolderColumnVisible: true,
    };
  },

  computed: {
    subfolderTreeItems() {
      if (this.treeRootItems != null) return this.treeRootItems;
      return this.materialItems || [];
    },
    filteredItems() {
      let filtered = this.materialItems;

      if (this.selectedType === 'favorites') {
        filtered = filtered.filter(itemInfo => this.favorites.includes(itemInfo.itemPath));
      }

      // Deep search mode: merge results from subfolders
      if (this.enableDeepSearch && this.searchQuery && this.deepSearchResults.length > 0) {
        // Get current folder items paths to avoid duplicates
        const currentPaths = new Set(filtered.map(item => item.itemPath));

        // Add deep search results that are not already in current items
        const uniqueDeepResults = this.deepSearchResults.filter(
          item => !currentPaths.has(item.itemPath)
        );

        // Merge: current folder items + deep search results from subfolders
        filtered = [...filtered, ...uniqueDeepResults];

        // Filter by search query
        const query = this.searchQuery.toLowerCase();
        filtered = filtered.filter(itemInfo => 
          itemInfo.itemName && itemInfo.itemName.toLowerCase().includes(query)
        );
      } else if (this.searchQuery) {
        // Normal search: filter current items only
        const query = this.searchQuery.toLowerCase();
        filtered = filtered.filter(itemInfo => itemInfo.itemName.toLowerCase().includes(query));
      }

      // 右侧只显示文件，文件夹全部在中间列二级目录显示
      filtered = filtered.filter(itemInfo => !(itemInfo.itemType && String(itemInfo.itemType).includes('folder')));

      return filtered;
    },

  },

  watch: {
    materialItems: {
      handler() {
        // Skip watcher entirely if any item is being edited to prevent blocking during typing
        const hasEditingItem = this.materialItems.some(item => item.isItemEditing);
        if (hasEditingItem) {
          return;
        }
        
        this.$nextTick(() => {
          this.filteredItems.forEach((itemInfo, index) => {
            this.updateCanvas(itemInfo, index);
          });
        });
      },
      deep: true,
    },

    // Redraw canvas when search query changes (filteredItems changes)
    filteredItems: {
      handler() {
        if (this.selectedType === 'in-model') {
          this.$nextTick(() => {
            this.filteredItems.forEach((itemInfo, index) => {
              this.updateCanvas(itemInfo, index);
            });
          });
        }
      },
    },

  },

  mounted() {
    // Load sidebar state
    const sidebarSaved = localStorage.getItem(sidebarKey);
    if (sidebarSaved !== null) {
      const sidebar = sidebarSaved === 'true';
      this.sidebarOpen = sidebar;
      this.sidebarState = sidebar ? 'sidebar-open' : 'sidebar-closed';
    }
    const savedWidth = localStorage.getItem(sidebarWidthKey);
    if (savedWidth) {
      const w = parseInt(savedWidth, 10);
      if (!isNaN(w) && w >= (typeof SIDEBAR_WIDTH_MIN !== 'undefined' ? SIDEBAR_WIDTH_MIN : 180) && w <= (typeof SIDEBAR_WIDTH_MAX !== 'undefined' ? SIDEBAR_WIDTH_MAX : 420)) {
        this.sidebarWidth = w;
      }
    }
    const savedSubfolderWidth = localStorage.getItem(subfolderColumnWidthKey);
    if (savedSubfolderWidth) {
      const w = parseInt(savedSubfolderWidth, 10);
      if (!isNaN(w) && w >= (typeof SUBFOLDER_WIDTH_MIN !== 'undefined' ? SUBFOLDER_WIDTH_MIN : 120) && w <= (typeof SUBFOLDER_WIDTH_MAX !== 'undefined' ? SUBFOLDER_WIDTH_MAX : 280)) {
        this.subfolderColumnWidth = w;
      }
    }
    const savedSubfolderVisible = localStorage.getItem(subfolderColumnVisibleKey);
    if (savedSubfolderVisible !== null) {
      this.subfolderColumnVisible = savedSubfolderVisible === 'true';
    }

    // Load online state（在线功能已移除，强制本地模式）
    this.isOnline = false;
    this.switchMode('local');

    // Load recent URL
    const recentUrl = localStorage.getItem(recentMaterialUrlKey);
    this.currentUrl = recentUrl || this.defaultSource[0];

    // Load local Storage
    this.loadDetailsState();
    this.loadViewOptions();

    // Update dynamic width
    updateDynamicWidth('material-app');

    // Drag & Sort - Initialize Sortable
    this.$nextTick(() => {
      this.initSortableLocal();
      this.initSortableOnline();

      this.filteredItems.forEach((itemInfo, index) => {
        this.setupCanvasObserver(itemInfo, index);
        this.updateCanvas(itemInfo, index);
      });
    });
  },

  beforeDestroy() {
    // Disconnect observers to prevent memory leaks
    if (this.resizeObservers) {
      this.resizeObservers.forEach(observer => observer.disconnect());
    }
  },

  methods: {
    initSortableLocal() {
      const sortableLocal = document.getElementById('sortableLocalMaterial');
      if (sortableLocal && !sortableLocal._sortableInstance) {
        sortableLocal._sortableInstance = new Sortable(sortableLocal, {
          animation: 150,
          onEnd: () => {
            this.handleSortLocal();
          }
        });
      }
    },

    initSortableOnline() {
      const sortableOnline = document.getElementById('sortableOnlineMaterial');
      if (sortableOnline && !sortableOnline._sortableInstance) {
        sortableOnline._sortableInstance = new Sortable(sortableOnline, {
          animation: 150,
          onEnd: () => {
            this.handleSortOnline();
          }
        });
      }
    },

    disableSortableLocal() {
      const sortableLocal = document.getElementById('sortableLocalMaterial');
      if (sortableLocal && sortableLocal._sortableInstance) {
        sortableLocal._sortableInstance.option('disabled', true);
      }
    },

    enableSortableLocal() {
      const sortableLocal = document.getElementById('sortableLocalMaterial');
      if (sortableLocal && sortableLocal._sortableInstance) {
        sortableLocal._sortableInstance.option('disabled', false);
      }
    },

    // Overridden from Mixin or specific to Material
    saveViewOptions() {
      const viewOptionsToSave = {
        currentViewIndex: this.currentViewIndex,
        thumbnailSize: this.thumbnailSize
      };
      // 使用全局视图设置键，四个页面同步缩略图视图
      localStorage.setItem(viewOptionsKey, JSON.stringify(viewOptionsToSave));
    },

    loadViewOptions() {
      // 从全局视图设置键加载，保持与其它页面同步
      const savedOptions = localStorage.getItem(viewOptionsKey);
      if (savedOptions) {
        const options = JSON.parse(savedOptions);
        this.currentViewIndex = options.currentViewIndex !== undefined ? options.currentViewIndex : 1;
        this.thumbnailSize = options.thumbnailSize || 100;
        this.applyViewOptions(this.viewOptions[this.currentViewIndex].mode);
      }
    },

    switchMode(value) {
      // 在线功能已移除，始终使用本地模式
      this.isOnline = false;
      const onlineState = 'local';
      localStorage.setItem(materialOnlineStateKey, onlineState);
    },

    startResizeSidebar(e) {
      this._resizeStartX = e.clientX;
      this._resizeStartWidth = this.sidebarWidth;
      document.addEventListener('mousemove', this._onResizeSidebarMove);
      document.addEventListener('mouseup', this._onResizeSidebarUp);
      e.preventDefault();
    },
    _onResizeSidebarMove(e) {
      const minW = typeof SIDEBAR_WIDTH_MIN !== 'undefined' ? SIDEBAR_WIDTH_MIN : 180;
      const maxW = typeof SIDEBAR_WIDTH_MAX !== 'undefined' ? SIDEBAR_WIDTH_MAX : 420;
      let w = this._resizeStartWidth + (e.clientX - this._resizeStartX);
      w = Math.max(minW, Math.min(maxW, w));
      this.sidebarWidth = w;
    },
    _onResizeSidebarUp() {
      document.removeEventListener('mousemove', this._onResizeSidebarMove);
      document.removeEventListener('mouseup', this._onResizeSidebarUp);
      if (typeof localStorage !== 'undefined' && typeof sidebarWidthKey !== 'undefined') {
        localStorage.setItem(sidebarWidthKey, String(this.sidebarWidth));
      }
    },

    startResizeSubfolderColumn(e) {
      this._resizeSubfolderStartX = e.clientX;
      this._resizeSubfolderStartWidth = this.subfolderColumnWidth;
      document.addEventListener('mousemove', this._onResizeSubfolderMove);
      document.addEventListener('mouseup', this._onResizeSubfolderUp);
      e.preventDefault();
    },
    _onResizeSubfolderMove(e) {
      const minW = typeof SUBFOLDER_WIDTH_MIN !== 'undefined' ? SUBFOLDER_WIDTH_MIN : 120;
      const maxW = typeof SUBFOLDER_WIDTH_MAX !== 'undefined' ? SUBFOLDER_WIDTH_MAX : 280;
      let w = this._resizeSubfolderStartWidth + (e.clientX - this._resizeSubfolderStartX);
      w = Math.max(minW, Math.min(maxW, w));
      this.subfolderColumnWidth = w;
    },
    _onResizeSubfolderUp() {
      document.removeEventListener('mousemove', this._onResizeSubfolderMove);
      document.removeEventListener('mouseup', this._onResizeSubfolderUp);
      if (typeof localStorage !== 'undefined' && typeof subfolderColumnWidthKey !== 'undefined') {
        localStorage.setItem(subfolderColumnWidthKey, String(this.subfolderColumnWidth));
      }
    },

    sortFolders() {
      this.sortOrderFolders = this.sortOrderFolders * -1;
      
      this.materialFolders.sort((a, b) => {
        if (a.customFolderName && b.customFolderName) {
          return this.sortOrderFolders === 1
            ? a.customFolderName.localeCompare(b.customFolderName)
            : b.customFolderName.localeCompare(a.customFolderName);
        } else if (a.customFolderName) {
          return -1;
        } else if (b.customFolderName) {
          return 1;
        }
        return 0;
      });

      const newOrder = this.materialFolders.map(folder => [folder.folderPath, folder.customFolderName]);
      sketchup.updateMaterialFolderOrder(JSON.stringify(newOrder));

      // Reinitialize Sortable after Vue re-renders the list
      this.$nextTick(() => {
        const sortableLocal = document.getElementById('sortableLocalMaterial');
        if (sortableLocal) {
          if (sortableLocal._sortableInstance) {
            sortableLocal._sortableInstance.destroy();
            sortableLocal._sortableInstance = null;
          }
          this.initSortableLocal();
        }
      });
    },

    sortedBy(value) {
      if (!this.materialItems || this.materialItems.length === 0) return;

      this.selectedSort = value;

      const folders = this.materialItems.filter(item => item.itemType && item.itemType.includes('folder'));
      const nonFolders = this.materialItems.filter(item => item.itemType && !item.itemType.includes('folder'));

      const sortFunction = (a, b) => {
        if (value === 'nameA') {
          return a.itemName.localeCompare(b.itemName, undefined, { numeric: true, sensitivity: 'base' });
        } else if (value === 'nameZ') {
          return b.itemName.localeCompare(a.itemName, undefined, { numeric: true, sensitivity: 'base' });
        } else if (value === 'sizeMin') {
          return this.parseSize(a.itemSize) - this.parseSize(b.itemSize);
        } else if (value === 'sizeMax') {
          return this.parseSize(b.itemSize) - this.parseSize(a.itemSize);
        }
        return 0;
      };

      folders.sort(sortFunction);
      nonFolders.sort(sortFunction);

      this.materialItems = [...folders, ...nonFolders];
      localStorage.setItem(materialSortedByKey, value);
    },

    parseSize(size) {
      // Handle number type directly
      if (typeof size === 'number') {
        return size;
      }

      if (typeof size !== 'string') {
        return null;
      }

      const trimmed = size.trim();

      // Handle "-" format (folders containing subfolders)
      if (trimmed === '-') {
        return -1; // Return -1 to sort these folders at the beginning or end
      }

      // Handle "1200 x 600" format (texture dimensions for in-model)
      if (size.includes(' x ')) {
        const parts = size.split(' x ');
        if (parts.length === 2) {
          const width = parseInt(parts[0].trim(), 10);
          const height = parseInt(parts[1].trim(), 10);
          if (!isNaN(width) && !isNaN(height)) {
            return width * height; // Return total pixels for comparison
          }
        }
        return null;
      }

      // Handle "10.0 MB" format (file size)
      if (size.includes('MB')) {
        return parseFloat(size.replace(' MB', '').trim());
      }

      // Handle pure number string (item count in folders, e.g., "49", "500")
      const num = parseInt(trimmed, 10);
      if (!isNaN(num)) {
        return num;
      }

      return null;
    },

    updateOnlineSources(data) {
      if (data) {
        this.onlineSources = data;
        if (this.onlineSources && this.onlineSources.length === 0 ) {
          this.currentUrl = '';
          localStorage.setItem(recentMaterialUrlKey, '');
        }
      }
    },

    openAddSource() {
      $("#materialSourceOnline").modal("show");
    },

    addSourceOnline() {
      const addNameInput = document.getElementById('materialAddNameInput');
      const nameValue = addNameInput.value.trim();
      const addUrlInput = document.getElementById('materialAddUrlInput');
      const urlValue = addUrlInput.value.trim();

      const urlPattern = new RegExp('^(https?:\\/\\/)?' + 
        '((([a-zA-Z0-9\\-]+\\.)+[a-zA-Z]{2,})|' + 
        '((\\d{1,3}\\.){3}\\d{1,3}))' + 
        '(\\:\\d+)?(\\/[-a-zA-Z0-9%_.~+]*)*' + 
        '(\\?[;&a-zA-Z0-9%_.~+=-]*)?' + 
        '(\\#[-a-zA-Z0-9_]*)?$', 'i');

      if (!nameValue) {
        alert('Source Name is required.');
        return;
      }

      if (!urlPattern.test(urlValue)) {
        alert('This is not a valid URL. Please try again!');
        return;
      }

      canEmbedInIframe(urlValue).then(canEmbed => {
        if (!canEmbed) {
          const message = this.$root.translated('siteBlocked');
          const action = () => { $("#materialSourceOnline").modal("show"); };
          this.$root.showNoticeModal(message, null, action);
        } else {
          const newSource = [urlValue, nameValue];
          if (!Array.isArray(this.onlineSources)) {
            this.onlineSources = [];
          }
    
          this.onlineSources.push(newSource);
          this.currentUrl = urlValue;

          localStorage.setItem(recentMaterialUrlKey, urlValue);
          $("#materialSourceOnline").modal("hide");
          sketchup.updateMaterialOnlineSources(this.onlineSources);
        }
      });
    },

    browseUrl(urlValue) {
      this.currentUrl = urlValue;
      localStorage.setItem(recentMaterialUrlKey, urlValue);
    },

    browseCollection(collection) {
      this.showShowEditMaterial = ['in-model', 'favorites', 'recent'].includes(collection);
      sketchup.browseMaterialCollection(collection);
      this.selectedFolderPath = null;
      this.recentFolderPath = null;
      this.selectedType = collection;
      this.switchMode('local');
    },

    addCollection() {
      sketchup.addMaterialCollection();
      this.recentFolderPath = null;
      this.selectedFolderPath = null;
      this.selectedType = null;
    },

    handleParentClick() {
      if (this.selectedType === 'in-model') {
        sketchup.parentMaterialActivePath();
      } else {
        this.parentFolder();
      }
    },

    handleItemClick(itemInfo) {
      if (itemInfo.itemType !== 'folder') {
        // Update selected materials
        this.selectedMaterials = [itemInfo.itemName];

        // Insert the material
        sketchup.insertMaterial(itemInfo.itemPath, itemInfo.itemType);

        // Update recent count
        const allRecent = document.getElementById('allMaterialRecent');
        if (allRecent) {
          const currentValue = parseInt(allRecent.textContent, 10) || 0;
          if (currentValue < 10) {
            allRecent.textContent = currentValue + 1;
          }
        }

      } else {
        this.browseFolder(itemInfo.itemPath, false);
        this.updateRecentChildFolder(itemInfo.itemPath);
      }
    },

    updateThumbnails() {
      const message = this.$root.translated('updateThumbnailsWaitingMessage');
      this.$root.showMessageModal(message, 'updating');
      setTimeout(() => {
        sketchup.updateMaterialThumbnails();
      }, 50);
    },

    updateRecentChildFolder(path) {
      localStorage.setItem(recentMaterialChildFolderKey, path);
    },

    updateRecentFolder(recentFolder) {
      this.showShowEditMaterial = ['in-model', 'favorites', 'recent'].includes(recentFolder);
      this.recentFolderPath = recentFolder;
      this.selectedType = recentFolder;
    },

    updateFolders(newMaterialFolders) {
      if (!Array.isArray(newMaterialFolders)) return;

      // 所有材质一级目录被删除时，清空二级目录树和当前浏览目录
      if (newMaterialFolders.length === 0) {
        this.materialFolders = [];
        this.selectedFolderPath = null;
        this.treeRootPath = null;
        this.treeRootItems = null;
        this.materialItems = [];
        this.folderEmpty = true;
        return;
      }

      // Initialize reactive properties for each folder
      newMaterialFolders.forEach(folder => {
        if (!folder.hasOwnProperty('isFolderEditing')) {
          this.$set(folder, 'isFolderEditing', false);
        }
        if (!folder.hasOwnProperty('newName')) {
          this.$set(folder, 'newName', '');
        }
      });

      // Always replace with new data from Ruby
      this.materialFolders = newMaterialFolders;

      // 如果当前没有选中任何目录（例如首次加载或切换到该页），自动选中第一个一级目录并同步浏览
      const hasSelection = !!(this.selectedFolderPath || this.treeRootPath);
      const isCollectionType = this.selectedType === 'in-model' || this.selectedType === 'favorites' || this.selectedType === 'recent';
      if (!hasSelection && !isCollectionType && newMaterialFolders.length > 0) {
        const firstPath = newMaterialFolders[0].folderPath;
        if (firstPath) {
          this.treeRootPath = firstPath;
          this.browseFolder(firstPath, false, true);
        }
      }

      // Re-initialize Sortable after Vue re-renders the list
      this.$nextTick(() => {
        // Destroy old Sortable instance if exists
        const sortableLocal = document.getElementById('sortableLocalMaterial');
        if (sortableLocal && sortableLocal._sortableInstance) {
          sortableLocal._sortableInstance.destroy();
          sortableLocal._sortableInstance = null;
        }
        this.initSortableLocal();
      });
    },

    updateItems(newMaterialItems) {
      if (newMaterialItems && newMaterialItems.length > 0) {
        // Initialize reactive properties for each item
        newMaterialItems.forEach(item => {
          if (!item.hasOwnProperty('isItemEditing')) {
            this.$set(item, 'isItemEditing', false);
          }
          if (!item.hasOwnProperty('newName')) {
            this.$set(item, 'newName', '');
          }
        });

        this.materialItems = newMaterialItems;
        this.folderEmpty = false;
        
        // Only auto-sort when loading new items (not when adding/renaming)
        if (this.autoSortEnabled) {
          const sortedSaved = localStorage.getItem(materialSortedByKey) || 'nameA';
          this.sortedBy(sortedSaved);
        }
      } else {
        this.materialItems = newMaterialItems || [];
        this.folderEmpty = true;
      }
      if (this.treeRootPath == null && this.selectedFolderPath && this._browseFromLeft) this.treeRootPath = this.selectedFolderPath;
      if (this.selectedFolderPath === this.treeRootPath) {
        this.treeRootItems = this.materialItems && this.materialItems.length ? this.materialItems.slice() : [];
      }

      // 当前目录若无文件仅有子文件夹，则自动显示下一级（第一个子文件夹）内容
      const items = newMaterialItems || [];
      const files = items.filter(item => !(item.itemType && String(item.itemType).includes('folder')));
      const folders = items.filter(item => item.itemType && String(item.itemType).includes('folder'));
      if (files.length === 0 && folders.length > 0) {
        this.$nextTick(() => { this.browseFolder(folders[0].itemPath, false); });
      }

      handleTooltip(this);
    },

    // Force refresh all thumbnails by adding timestamp (used after update_thumbnails)
    forceRefreshThumbnails() {
      forceHideAllModals();
      const timestamp = '?t=' + Date.now();
      this.materialItems.forEach(item => {
        if (item.thumbnailPath) {
          // Remove old timestamp if exists, then add new one
          const basePath = item.thumbnailPath.split('?')[0];
          this.$set(item, 'thumbnailPath', basePath + timestamp);
        }
      });
    },

    // Targeted update for a single renamed item (avoid full refresh)
    updateRenamedItem(result) {
      if (!result) return;

      const { oldPath, newPath, newName, thumbnailPath } = result;
      const itemInfo = this.materialItems.find(item => item.itemPath === oldPath);

      if (itemInfo) {
        // Update item properties reactively
        this.$set(itemInfo, 'itemPath', newPath);
        this.$set(itemInfo, 'itemName', newName);
        this.$set(itemInfo, 'thumbnailPath', thumbnailPath + '?t=' + Date.now());
        this.$set(itemInfo, 'isItemEditing', false);

        // Update favorites if the old path was in favorites
        const favIndex = this.favorites.indexOf(oldPath);
        if (favIndex > -1) {
          this.favorites.splice(favIndex, 1, newPath);
          sketchup.updateMaterialFavorites(newPath, itemInfo.itemType);
        }
      }
    },

    // Add a single new item to the end of the list (avoid full refresh)
    addNewItem(itemData) {
      if (!itemData) return;

      // Add timestamp to thumbnail to prevent caching
      if (itemData.thumbnailPath) {
        itemData.thumbnailPath = itemData.thumbnailPath + '?t=' + Date.now();
      }

      // Initialize reactive properties
      this.$set(itemData, 'isItemEditing', false);
      this.$set(itemData, 'newName', '');

      // Simply append to the end of the list (no auto-sorting)
      this.materialItems.push(itemData);
    },

    // Update a single material item when edited from Material Editor dialog
    // Only update if viewing in-model collection to avoid unnecessary updates
    updateSingleMaterialItem(itemData) {
      if (!itemData || this.selectedType !== 'in-model') return;

      const itemInfo = this.materialItems.find(item => item.itemPath === itemData.itemPath);

      if (itemInfo) {
        // Update item properties reactively
        this.$set(itemInfo, 'itemColor', itemData.itemColor);
        this.$set(itemInfo, 'itemDeltas', itemData.itemDeltas);
        this.$set(itemInfo, 'itemType', itemData.itemType);
        this.$set(itemInfo, 'itemSize', itemData.itemSize);
        // Add timestamp to thumbnail to force refresh
        this.$set(itemInfo, 'thumbnailPath', itemData.thumbnailPath + '?t=' + Date.now());

        // Update canvas for this item
        const index = this.materialItems.indexOf(itemInfo);
        if (index !== -1) {
          this.$nextTick(() => {
            this.updateCanvas(itemInfo, index);
          });
        }
      }
    },

    // Find the correct position to insert new item based on current sort order
    findInsertIndex(newItem) {
      const sortValue = this.selectedSort || 'nameA';
      const isFolder = newItem.itemType && newItem.itemType.includes('folder');

      // Separate folders and non-folders
      let startIndex = 0;
      let endIndex = this.materialItems.length;

      if (isFolder) {
        // Find the end of folders section
        endIndex = this.materialItems.findIndex(item => 
          !item.itemType || !item.itemType.includes('folder')
        );
        if (endIndex === -1) endIndex = this.materialItems.length;
      } else {
        // Find the start of non-folders section
        startIndex = this.materialItems.findIndex(item => 
          !item.itemType || !item.itemType.includes('folder')
        );
        if (startIndex === -1) startIndex = this.materialItems.length;
      }

      // Find correct position within the section
      for (let i = startIndex; i < endIndex; i++) {
        const compareItem = this.materialItems[i];
        let shouldInsertBefore = false;

        if (sortValue === 'nameA') {
          shouldInsertBefore = newItem.itemName.localeCompare(
            compareItem.itemName, undefined, { numeric: true, sensitivity: 'base' }
          ) < 0;
        } else if (sortValue === 'nameZ') {
          shouldInsertBefore = newItem.itemName.localeCompare(
            compareItem.itemName, undefined, { numeric: true, sensitivity: 'base' }
          ) > 0;
        } else if (sortValue === 'sizeMin') {
          shouldInsertBefore = this.parseSize(newItem.itemSize) < this.parseSize(compareItem.itemSize);
        } else if (sortValue === 'sizeMax') {
          shouldInsertBefore = this.parseSize(newItem.itemSize) > this.parseSize(compareItem.itemSize);
        }

        if (shouldInsertBefore) {
          return i;
        }
      }

      return endIndex;
    },

    updateTotalInModel(total) {
      const inModel = document.getElementById('materialInModel');
      if (inModel) {
        inModel.textContent = total;
      }
    },

    updateFavorites(favorites) {
      this.favorites = favorites;
      const allFavorites = document.getElementById('allMaterialFavorites');
      if (allFavorites) {
        allFavorites.textContent = this.favorites.length;
      }
    },

    toggleFavorite(itemInfo) {
      if (itemInfo.itemType !== 'folder') {
        const index = this.favorites.indexOf(itemInfo.itemPath);
        if (index > -1) {
          this.favorites.splice(index, 1);
        } else {
          this.favorites.push(itemInfo.itemPath);
        }

        const allFavorites = document.getElementById('allMaterialFavorites');
        if (allFavorites) {
          allFavorites.textContent = this.favorites.length;
        }

        sketchup.updateMaterialFavorites(itemInfo.itemPath, itemInfo.itemType);
      } else {
        sketchup.addMaterialCollection(itemInfo.itemPath);
      }
    },

    updateRecent(recent) {
      this.recent = recent;
      const allRecent = document.getElementById('allMaterialRecent');
      if (allRecent) {
        allRecent.textContent = recent.length > 10 ? 10 : recent.length;
      }
    },

    parentFolder() {
      const recentChildFolder = localStorage.getItem(recentMaterialChildFolderKey);
      sketchup.browseMaterialFolder(recentChildFolder, true);
    },

    /** 材质页 +：启动 D7 吸管，保存目录为当前库（点击表面吸取入库，非选择集批量导出） */
    addSelected(folderPath) {
      const target = folderPath || this.selectedFolderPath || this.activeFolderPath;
      if (!target) {
        const message = this.$root.translated('selectFolderFirst') || 'Please select a folder first';
        this.$root.showMessageModal(message, 'info', 1500);
        return;
      }
      sketchup.addMaterialSelected(target);
    },

    browseFolder(folderPath, parent, fromLeft) {
      this._browseFromLeft = !!fromLeft;
      this.showShowEditMaterial = false;
      sketchup.browseMaterialFolder(folderPath, parent);
      this.selectedFolderPath = folderPath;
      this.recentFolderPath = null;
      this.selectedType = null;
      this.switchMode('local');
    },

    onLeftBrowseFolder(folderPath) {
      this.treeRootPath = folderPath;
      this.browseFolder(folderPath, false, true);
    },

    selectedFolder(folderPath) {
      this.selectedFolderPath = folderPath;
    },

    updateImagesFolderPath(totalImages) {
      this.initAddMaterialOptions(totalImages);
    },

    // Context Menus
    showCollectionContextMenu(event, folderPath, folderName) {
      // Hide other context menus
      this.hideContextMenu();

      // Display context menu
      this.folderContextMenuVisible = true;

      const menuHeight = 216;
      const dialogHeight = window.innerHeight;
      const buffer = 10;

      // Get position Y of menu
      let calculatedY = event.clientY;

      // Distance from click pos to dialog bottom
      const distanceToBottom = dialogHeight - event.clientY;

      // If menu height is over dialog bottom, re-position Y
      if (distanceToBottom < menuHeight + buffer) {
        calculatedY = dialogHeight - menuHeight - buffer;
      }

      // Update new position of menu
      this.folderContextMenuPosition.x = event.clientX;
      this.folderContextMenuPosition.y = calculatedY;

      // Save selected folder
      this.actionFolderPath = folderPath;
      this.selectedFolderName = folderName;
    },

    collectionActions(action) {
      if (action === 'rename') {
        this.renameCollection(this.actionFolderPath);
      } else if (action === 'remove') {
        this.removeCollection(this.actionFolderPath);
      } else if (action === 'add_separator') {
        this.addSeparator(this.actionFolderPath);
      } else if (action === 'open_location') {
        sketchup.openFolder(this.actionFolderPath);
      };
      this.folderContextMenuVisible = false;
    },

    renameCollection(folderPath) {
      // Close all other editing folders first
      this.materialFolders.forEach(folder => {
        if (folder.isFolderEditing) {
          this.$set(folder, 'isFolderEditing', false);
        }
      });

      const folderInfo = this.materialFolders.find(folder => folder.folderPath === folderPath);
      if (folderInfo) {
        // Disable Sortable to allow text selection in input
        this.disableSortableLocal();

        this.$set(folderInfo, 'newName', folderInfo.customFolderName);
        this.$set(folderInfo, 'isFolderEditing', true);

        this.$nextTick(() => {
          // Emit event to sidebar to focus the input
          this.$root.$emit('focus-folder-input', folderInfo.folderPath);
        });
      }
    },

    saveNewCollectionName(folderPath, oldFolderName) {
      const folderInfo = this.materialFolders.find(folder => folder.folderPath === folderPath);

      if (folderInfo && folderInfo.isFolderEditing) {
        let editedName;
        let newFolderName;
        if (folderInfo.newName) {
          newFolderName = folderInfo.newName.trim();
          if (newFolderName !== '' && newFolderName !== oldFolderName) {
            editedName = newFolderName;
          } else {
            editedName = folderInfo.customFolderName;
          }
        } else {
          editedName = folderInfo.originFolderName;
        };

        this.$set(folderInfo, 'isFolderEditing', false);
        
        // Re-enable Sortable after editing is done
        this.enableSortableLocal();
        
        sketchup.renameMaterialCollection(folderPath, editedName);
      }
    },

    removeCollection(folderPath) {
      // 如果当前二级目录树的根或当前选中目录就在被删除的集合下，先清空本地状态
      const base = (folderPath || '').toString().replace(/\\/g, '/').replace(/\/+$/, '');
      const selected = (this.selectedFolderPath || '').toString().replace(/\\/g, '/');
      const treeRoot = (this.treeRootPath || '').toString().replace(/\\/g, '/');

      if (base && (selected === base || selected.startsWith(base + '/') || treeRoot === base || treeRoot.startsWith(base + '/'))) {
        this.selectedFolderPath = null;
        this.treeRootPath = null;
        this.treeRootItems = null;
        this.materialItems = [];
        this.folderEmpty = true;
      }

      sketchup.removeMaterialCollection(folderPath);
    },

    addSeparator(folderPath) {
      // Find the folder element in DOM
      const list = document.getElementById('sortableLocalMaterial');
      const items = list.getElementsByTagName('li');
      let targetItem = null;
      
      for (let i = 0; i < items.length; i++) {
        if (items[i].getAttribute('data-folder-path') === folderPath) {
          targetItem = items[i];
          break;
        }
      }
      
      if (targetItem) {
        // Create separator element directly in DOM
        const separatorLi = document.createElement('li');
        separatorLi.setAttribute('data-folder-path', '|-----|');
        separatorLi.className = 'li-separator';
        separatorLi.innerHTML = `
          <div class="dropdown-divider custom-dropdown-divider"></div>
          <span class="folder-name"></span>
        `;
        
        // Add context menu handler
        separatorLi.addEventListener('contextmenu', (event) => {
          event.stopPropagation();
          event.preventDefault();
          this.showSeparatorContextMenu(event);
        });
        
        // Insert after target item
        targetItem.parentNode.insertBefore(separatorLi, targetItem.nextSibling);
        
        // Save the new order to Ruby
        this.handleSortLocal();
      }
    },

    removeSeparator() {
      const separator = this.separatorElement;
      if (separator) {
        separator.remove();
        this.$nextTick(() => {
          this.handleSortLocal();
        });
      }
      this.separatorContextMenuVisible = false;
    },

    showItemContextMenu(event, itemInfo) {
      this.selectedItemType = itemInfo.itemType !== 'folder' ? 'item' : 'folder';

      // Hide other context menus
      this.hideContextMenu();

      // Check item type and save selected item BEFORE showing menu
      this.isSketchupMaterial = itemInfo.itemType !== 'folder';
      this.selectedItem = itemInfo;

      // Display context menu
      this.itemContextMenuVisible = true;

      // Set temporary position at top-left corner to measure true dimensions
      // (placing at click position may cause menu to be clipped by viewport,
      // resulting in getBoundingClientRect returning truncated dimensions)
      this.itemContextMenuPosition = {
        x: 0,
        y: 0
      };

      // Use $nextTick to ensure that the menu is rendered before calculating its dimensions
      this.$nextTick(() => {
        // Query the actual menu element from DOM
        const menuElements = document.querySelectorAll('.context-menu.custom-menu');
        let menuElement = null;

        // Find the visible item context menu
        menuElements.forEach(el => {
          if (el.style.display !== 'none' && window.getComputedStyle(el).display !== 'none') {
            const hasItemContent = el.querySelector('.custom-span');
            if (hasItemContent && this.itemContextMenuVisible) {
              menuElement = el;
            }
          }
        });

        if (!menuElement) return;

        // Calculate real menu dimensions (now measured without viewport clipping)
        const menuRect = menuElement.getBoundingClientRect();
        const menuWidth = menuRect.width;
        const menuHeight = menuRect.height;

        // Get the viewport dimensions
        const dialogWidth = window.innerWidth;
        const dialogHeight = window.innerHeight;
        const buffer = 10;

        // Get position of menu
        let calculatedX = event.clientX;
        let calculatedY = event.clientY;

        // Distance from click pos to dialog right & bottom
        const distanceToRight = dialogWidth - event.clientX;
        const distanceToBottom = dialogHeight - event.clientY;

        // If menu width is over dialog right, re-position X
        if (distanceToRight < menuWidth + buffer) {
          calculatedX = dialogWidth - menuWidth - buffer;
        }

        // If menu height is over dialog bottom, re-position Y
        if (distanceToBottom < menuHeight + buffer) {
          calculatedY = dialogHeight - menuHeight - buffer;
        }

        // Ensure menu doesn't go beyond left or top edge
        if (calculatedX < buffer) {
          calculatedX = buffer;
        }
        if (calculatedY < buffer) {
          calculatedY = buffer;
        }

        // Update new position of menu
        this.itemContextMenuPosition.x = calculatedX;
        this.itemContextMenuPosition.y = calculatedY;
      });
    },

    getSelectedItems() {
      this.selectedItems = [this.selectedItem.itemPath];
      return this.selectedItems;
    },

    handleQuickAction(payload) {
      const { action, itemInfo } = payload || {};
      if (!itemInfo || itemInfo.itemType === 'folder') return;

      this.selectedItem = itemInfo;

      if (action === 'replace_material') {
        if (this.selectedType === 'in-model') {
          sketchup.replaceMaterial(itemInfo.itemPath, 'in-model');
        } else {
          sketchup.replaceMaterial(itemInfo.itemPath, 'library');
        }
      } else if (action === 'replace_cabinet_layer') {
        sketchup.replaceCabinetMaterial(itemInfo.itemPath, itemInfo.itemType);
      } else if (action === 'replace_door_panel_layer') {
        sketchup.replaceDoorPanelMaterial(itemInfo.itemPath, itemInfo.itemType);
      }
    },

    itemActions(action) {
      if (action === 'import_material') {
        this.targetMaterialFolderPath = this.selectedItem.itemPath;
        sketchup.browseImagesFolder();

      } else if (action === 'replace_material') {
        if (this.selectedType === 'in-model') {
          sketchup.replaceMaterial(this.selectedItem.itemPath, 'in-model');
        } else {
          sketchup.replaceMaterial(this.selectedItem.itemPath, 'library');
        }

      } else if (action === 'edit_material') {
        this.editMaterial(this.selectedItem.itemPath);

      } else if (action === 'resize_textures') {
        this.getSelectedItems();
        this.initResizeTexturesOptions();

      } else if (action === 'save_as') {
        sketchup.saveMaterial(this.getSelectedItems());

      } else if (action === 'rename') {
        this.renameItem(this.selectedItem);

      } else if (action === 'delete') {
        this.getSelectedItems();
        this.confirmDeleteItems(this.selectedItem.itemType);

      } else if (action === 'purge_unused') {
        sketchup.purgeUnusedMaterials();

      } else if (action === 'remove_recent') {
        sketchup.removeRecentMaterials(this.getSelectedItems());

      } else if (action === 'open_location') {
        sketchup.openFileLocation(this.selectedItem.itemPath);
      }

      // Hide context menu
      this.itemContextMenuVisible = false;
    },

    editMaterial(itemPath) {
      sketchup.editMaterial(itemPath);
    },

    renameItem(itemInfo) {
      // Close all other editing items first
      this.materialItems.forEach(item => {
        if (item.isItemEditing) {
          this.$set(item, 'isItemEditing', false);
        }
      });

      this.$set(itemInfo, 'newName', itemInfo.itemName.replace('.skm', ''));
      this.$set(itemInfo, 'isItemEditing', true);
      this.$set(itemInfo, 'showingError', false);

      this.$nextTick(() => {
        // Emit event to content component to focus the input
        this.$root.$emit('focus-item-input', itemInfo.itemName);
      });
    },

    cancelRenameItem(itemPath, oldItemName) {
      const itemInfo = this.materialItems.find(item => item.itemPath === itemPath);
      if (itemInfo && itemInfo.isItemEditing) {
        this.$set(itemInfo, 'itemName', oldItemName);
        this.$set(itemInfo, 'isItemEditing', false);
      }
    },

    saveNewItemName(itemPath, oldItemName) {
      // Prevent multiple simultaneous calls
      if (this.isSavingName) {
        return;
      }

      const itemInfo = this.materialItems.find(item => item.itemPath === itemPath);

      if (itemInfo && itemInfo.isItemEditing) {
        if (itemInfo.newName) {
          const newItemName = itemInfo.newName.trim();

          // Check for invalid characters in file name (Windows and Mac)
          if (!this.validateFileName(newItemName, oldItemName)) {
            return;
          }

          // Check same name with any other items
          this.nameExists = this.materialItems.some(item => 
            item.itemName.toLowerCase().replace('.skm', '') === newItemName.toLowerCase() &&
            item.itemPath !== itemPath
          );

          // Show alert if name exists
          if (this.nameExists) {
            // Keep editing mode but show error message only once
            if (!itemInfo.showingError) {
              this.isSavingName = true; // Lock
              this.$set(itemInfo, 'showingError', true);
              const message = this.$root.translated('duplicateNameExistsAlert');
              this.$root.showMessageModal(message, 'error', 1000);
              // Wait for modal to close before focusing back to input and reset error flag
              setTimeout(() => {
                this.$set(itemInfo, 'showingError', false);
                this.$root.$emit('focus-item-input', oldItemName);
                this.isSavingName = false; // Unlock after focus
              }, 1200);
            }
            return;
          }

          if (newItemName !== '' && newItemName !== oldItemName) {
            const item = [itemInfo, oldItemName, newItemName];
            sketchup.renameMaterialItem(item);

            // Note: Don't update itemPath or thumbnailPath here.
            // Ruby will call Material.refresh(true) which triggers updateItems()
            // with the correct new paths including properly renamed thumbnail.
          }
        }

        this.$set(itemInfo, 'isItemEditing', false);
      }
    },

    // 'type': 'skm', 'folder', and other names means 'in-model'
    confirmDeleteItems(type) {
      let message;
      let action1 = () => null;
      let action2;

      if (type === 'skm') {
        message = this.translated('confirmDeleteMessage');
        action2 = () => sketchup.deleteMaterialItems(this.selectedItems);
      } else if (type === 'folder') {
        message = this.translated('confirmDeleteFolderMessage');
        action2 = () => sketchup.deleteFolders(this.selectedItems);
      } else {
        message = this.translated('confirmDeleteMaterialInModel');
        action2 = () => sketchup.deleteMaterialInModel(this.selectedItems);
      }

      this.$root.showNoticeModal(message, action1, action2);
    },

    updateDeleteItems(itemPaths) {
      itemPaths.forEach(itemPath => {
        // Remove from materialItems
        const index = this.materialItems.findIndex(item => item.itemPath === itemPath);
        if (index > -1) {
          this.materialItems.splice(index, 1);
        }

        // Remove from favorites
        const favIndex = this.favorites.indexOf(itemPath);
        if (favIndex > -1) {
          this.favorites.splice(favIndex, 1);
        }
      });

      // Update favorites count in UI
      const allFavorites = document.getElementById('allMaterialFavorites');
      if (allFavorites) {
        allFavorites.textContent = this.favorites.length;
      }
    },

    showOnlineContextMenu(event, source) {
      // Hide other context menus
      this.hideContextMenu();

      // Save selected item BEFORE showing menu
      this.selectedOnlineName = source[1];
      this.selectedOnlineUrl = source[0];

      // Display context menu
      this.onlineContextMenuVisible = true;

      // Use $nextTick to ensure that the menu is rendered before calculating its dimensions
      this.$nextTick(() => {
        // Query the actual menu element from DOM
        const menuElements = document.querySelectorAll('.context-menu.custom-menu');
        let menuElement = null;
        
        // The online menu should be the last visible one
        for (let i = menuElements.length - 1; i >= 0; i--) {
          if (menuElements[i].style.display !== 'none' && this.onlineContextMenuVisible) {
            menuElement = menuElements[i];
            break;
          }
        }

        if (!menuElement) return;

        // Calculate real menu dimensions
        const menuRect = menuElement.getBoundingClientRect();
        const menuWidth = menuRect.width;
        const menuHeight = menuRect.height;

        // Get the viewport dimensions
        const dialogWidth = window.innerWidth;
        const dialogHeight = window.innerHeight;
        const buffer = 10;

        // Get position of menu
        let calculatedX = event.clientX;
        let calculatedY = event.clientY;

        // Distance from click pos to dialog right & bottom
        const distanceToRight = dialogWidth - event.clientX;
        const distanceToBottom = dialogHeight - event.clientY;

        // If menu width is over dialog right, re-position X
        if (distanceToRight < menuWidth + buffer) {
          calculatedX = dialogWidth - menuWidth - buffer;
        }

        // If menu height is over dialog bottom, re-position Y
        if (distanceToBottom < menuHeight + buffer) {
          calculatedY = dialogHeight - menuHeight - buffer;
        }

        // Update new position of menu
        this.onlineContextMenuPosition.x = calculatedX;
        this.onlineContextMenuPosition.y = calculatedY;
      });
    },

    onlineActions(action) {
      if (action === 'rename') {
        this.renameOnlineSource(this.selectedOnlineUrl, this.selectedOnlineName);
      } else if (action === 'delete') {
        this.deleteOnlineSource(this.selectedOnlineUrl);
      }
      this.onlineContextMenuVisible = false;
    },

    renameOnlineSource(url, name) {
      const source = this.onlineSources.find(s => s[0] === url);
      if (source) {
        this.$set(source, 'newName', name);
        this.$set(source, 'isOnlineEditing', true);

        // Gọi method của online-container component để focus input
        this.$nextTick(() => {
          if (this.$refs.onlineContainer) {
            this.$refs.onlineContainer.focusInput(name);
          }
        });
      }
    },

    saveNewOnlineSourceName(url, oldName) {
      const source = this.onlineSources.find(s => s[0] === url);
      if (source) {
        let newName = source.newName ? source.newName.trim() : oldName;
        if (!newName) newName = oldName;

        source[1] = newName;
        this.$set(source, 'isOnlineEditing', false);
        sketchup.updateMaterialOnlineSources(this.onlineSources);
      }
    },

    deleteOnlineSource(url) {
      this.onlineSources = this.onlineSources.filter(s => s[0] !== url);
      if (this.currentUrl === url) {
        this.currentUrl = this.defaultSource[0];
        localStorage.setItem(recentMaterialUrlKey, this.defaultSource[0]);
      }
      sketchup.updateMaterialOnlineSources(this.onlineSources);
    },

    showSeparatorContextMenu(event) {
      // Hide other context menus
      this.hideContextMenu();

      // Display context menu
      this.separatorContextMenuVisible = true;

      // Define separator
      this.separatorElement = event.currentTarget.closest('li');

      const menuHeight = 60;
      const dialogHeight = window.innerHeight;
      const buffer = 10;

      // Get position Y of menu
      let calculatedY = event.clientY;

      // Distance from click pos to dialog bottom
      const distanceToBottom = dialogHeight - event.clientY;

      // If menu height is over dialog bottom, re-position Y
      if (distanceToBottom < menuHeight + buffer) {
        calculatedY = dialogHeight - menuHeight - buffer;
      }

      // Update new position of menu
      this.separatorContextMenuPosition.x = event.clientX;
      this.separatorContextMenuPosition.y = calculatedY;
    },

    showContentContextMenu(event) {
      if (this.selectedType === 'favorites' || this.selectedType === 'recent') {
        return;
      }

      // Hide other context menus
      this.hideContextMenu();

      // Display context menu
      this.contentContextMenuVisible = true;

      // Use $nextTick to ensure that the menu is rendered before calculating its dimensions
      this.$nextTick(() => {
        // Query the actual menu element from DOM
        const menuElements = document.querySelectorAll('.context-menu.custom-menu');
        let menuElement = null;
        
        // Find visible content menu
        for (let i = menuElements.length - 1; i >= 0; i--) {
          if (menuElements[i].style.display !== 'none' && this.contentContextMenuVisible) {
            menuElement = menuElements[i];
            break;
          }
        }

        if (!menuElement) return;

        // Calculate real menu dimensions
        const menuRect = menuElement.getBoundingClientRect();
        const menuWidth = menuRect.width;
        const menuHeight = menuRect.height;

        // Get parent container offset
        const appContainer = document.getElementById('material-app');
        const containerOffset = appContainer ? appContainer.getBoundingClientRect() : { top: 0, left: 0 };

        // Get the viewport dimensions
        const dialogWidth = window.innerWidth;
        const dialogHeight = window.innerHeight;
        const buffer = 10;

        // Get position of menu - adjust for container offset
        let calculatedX = event.clientX - containerOffset.left;
        let calculatedY = event.clientY - containerOffset.top;

        // Distance from click pos to dialog right & bottom
        const distanceToRight = dialogWidth - event.clientX;
        const distanceToBottom = dialogHeight - event.clientY;

        // If menu width is over dialog right, re-position X
        if (distanceToRight < menuWidth + buffer) {
          calculatedX = dialogWidth - menuWidth - buffer - containerOffset.left;
        }

        // If menu height is over dialog bottom, re-position Y
        if (distanceToBottom < menuHeight + buffer) {
          calculatedY = dialogHeight - menuHeight - buffer - containerOffset.top;
        }

        // Update new position of menu
        this.contentContextMenuPosition.x = calculatedX;
        this.contentContextMenuPosition.y = calculatedY;
      });
    },

    contentActions(action) {
      if (action === 'import_material') {
        if (this.selectedType === 'in-model') {
          sketchup.importMaterialToModel();
        } else {
          this.targetMaterialFolderPath = this.activeFolderPath;
          sketchup.browseImagesFolder();
        }
      } else if (action === 'create_material') {
        sketchup.createMaterial();
      } else if (action === 'purge_unused') {
        sketchup.purgeUnusedMaterials();
      }
      this.contentContextMenuVisible = false;
    },

    handleSortLocal() {
      const list = document.getElementById('sortableLocalMaterial');
      const items = list.getElementsByTagName('li');
      const newOrder = [];
      
      for (let i = 0; i < items.length; i++) {
        const folderPath = items[i].getAttribute('data-folder-path');
        const element = items[i].querySelector('.folder-name');
        const folderName = (element && element.textContent) || '';
        if (folderPath) {
          newOrder.push([folderPath, folderName]);
        }
      }
      // Only save to Ruby, don't update Vue data to avoid conflict with Sortable
      sketchup.updateMaterialFolderOrder(JSON.stringify(newOrder));
    },

    handleSortOnline() {
      const list = document.getElementById('sortableOnlineMaterial');
      const items = list.getElementsByTagName('button');
      const newOrder = [];

      for (let i = 0; i < items.length; i++) {
        const url = items[i].getAttribute('data-url');
        const name = items[i].textContent.trim();
        if (url) {
          newOrder.push([url, name]);
        }
      }

      this.onlineSources = newOrder;
      sketchup.updateMaterialOnlineSources(this.onlineSources);
    },

    newFolder(path) {
      itemInfo = {
        itemPath: path + '/' + 'New Folder',
        itemName: 'New Folder',
        itemType: 'folder',
        itemSize: 0,
        thumbnailPath: folderThumbnailPath
      }
      sketchup.newFolder(itemInfo.itemPath);
    },

    updateSelected(value) {
      this.selectedMaterials = value;
    },

    updateActiveFolder(value) {
      this.activeFolderPath = value;
    },

    updateIsDrive(value) {
      this.isDrive = value;

      // if (!this.isDrive) return;

      // const message = this.$root.translated('isDriveNotice');
      // this.$root.showMessageModal(message, 'info', 1500);
    },

    // Deep Search Methods
    handleDeepSearch() {
      if (this.isDrive) {
        const message = this.$root.translated('deepSearchDriveNotSupported');
        this.$root.showMessageModal(message, 'info', 2000);
        return;
      }

      if (!this.enableDeepSearch) return;
      if (!this.searchQuery || !this.searchQuery.trim()) return;
      if (!this.activeFolderPath) {
        const message = this.$root.translated('selectFolderFirst') || 'Please select a folder first';
        this.$root.showMessageModal(message, 'info', 1500);
        return;
      }

      // Don't search in special collections
      if (this.selectedType === 'in-model' || this.selectedType === 'favorites' || this.selectedType === 'recent') {
        return;
      }

      // sketchup.checkFolderSize(this.activeFolderPath);
      const message = this.$root.translated('deepSearching');
      this.$root.showMessageModal(message, 'deepSearching', 'wait');

      // Delay to allow modal to render
      setTimeout(() => {
        // this.setDeepSearchLoading(true);
        this.performDeepSearch();
      }, 500);
    },

    performDeepSearch() {
      sketchup.deepSearchMaterial(this.searchQuery.trim(), this.activeFolderPath);
    },

    updateDeepSearchResults(items) {
      this.deepSearchResults = items || [];

      if (this.deepSearchResults.length === 0) {
        const message1 = this.$root.translated('noDeepSearchResults');
        const message2 = this.$root.translated('searchWithinSubfolders');
        const message = `${message1}<br><br>${message2}`;
        this.$root.showNoticeModal(message);
      } else {
        forceHideAllModals();
      }

      this.isDeepSearching = false;
    },

    setDeepSearchLoading(isLoading) {
      this.isDeepSearching = isLoading;
    },

    clearDeepSearch() {
      this.deepSearchResults = [];
      this.isDeepSearching = false;
    },

    loadDetailsState() {
      this.showDetails = true;
    },

    saveDetailsState(_newValue) {
      this.showDetails = true;
    },

    onEnterKey(folderPath, folderName) {
      this.saveNewCollectionName(folderPath, folderName);
    },

    onBlur(folderPath, folderName) {
      // Use setTimeout to ensure blur is processed after other events
      this.$nextTick(() => {
        this.saveNewCollectionName(folderPath, folderName);
      });
    },

    cancelRename(folderPath) {
      const folderInfo = this.materialFolders.find(folder => folder.folderPath === folderPath);
      if (folderInfo && folderInfo.isFolderEditing) {
        // Restore original name and exit edit mode
        this.$set(folderInfo, 'newName', folderInfo.customFolderName);
        this.$set(folderInfo, 'isFolderEditing', false);
        
        // Re-enable Sortable
        this.enableSortableLocal();
      }
    },

    setupCanvasObserver(itemInfo, index) {
      const canvas = document.getElementById('textureCanvas' + index);
      if (!canvas) return;

      const resizeObserver = new ResizeObserver(entries => {
        for (let entry of entries) {
          // Debounce if necessary
          this.updateCanvas(itemInfo, index);
        }
      });

      resizeObserver.observe(canvas);

      // Store the observer
      this.resizeObservers.push(resizeObserver);
    },

    updateCanvas(itemInfo, index) {
      if (!itemInfo) {
        return;
      }

      let colorized = itemInfo.itemType.includes('true');;
      let deltas = itemInfo.itemDeltas;
      const img = new Image();

      const basePath = (itemInfo.thumbnailPath || '').split('?')[0];
      const url = window.fileUrlFromPath ? window.fileUrlFromPath(basePath) : ('file:///' + (basePath || '').replace(/\\/g, '/'));
      if (this.forceRender) {
        img.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 't=' + new Date().getTime();
      } else {
        img.src = url;
      }

      img.onload = () => {
        const canvas = document.getElementById('textureCanvas' + index);
        if (canvas) {
          const ctx = canvas.getContext('2d', { willReadFrequently: true });

          // Get the computed display size
          canvas.width = 256;
          canvas.height = 256;

          const canvasWidth = canvas.width;
          const canvasHeight = canvas.height;
          const imgWidth = img.width;
          const imgHeight = img.height;

          // Handle ratio
          const imgRatio = imgWidth / imgHeight;
          const canvasRatio = canvasWidth / canvasHeight;

          let drawWidth, drawHeight;

          // Check ratio and adjust dimension
          if (imgRatio > canvasRatio) {
            drawWidth = canvasWidth;
            drawHeight = canvasWidth / imgRatio;
          } else {
            drawHeight = canvasHeight;
            drawWidth = canvasHeight * imgRatio;
          }

          // Calculate position to center image
          const x = (canvasWidth - drawWidth) / 2;
          const y = (canvasHeight - drawHeight) / 2;

          // Calculate for overlay
          if (itemInfo.itemColor) {
            const leftPercent = (x / canvasWidth) * 100;
            const topPercent = (y / canvasHeight) * 100;
            const widthPercent = (drawWidth / canvasWidth) * 100;
            const heightPercent = (drawHeight / canvasHeight) * 100;
            const opacity = 1 - itemInfo.itemColor[3];
            this.$set(this.overlayStyles, index, {
              position: 'absolute',
              top: `${topPercent}%`,
              left: `${leftPercent}%`,
              width: `${widthPercent}%`,
              height: `${heightPercent}%`,
              backgroundColor: `rgba(255, 255, 255, ${opacity})`
            });
          }

          // Draw image into canvas at position
          ctx.clearRect(0, 0, canvasWidth, canvasHeight);
          ctx.drawImage(img, x, y, drawWidth, drawHeight);

          if (deltas) {
            // Get image data from canvas
            let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            let data = imageData.data;

            // Get HSL deltas from SketchUp
            const deltaH = deltas[0];
            const deltaS = Math.round(deltas[1] * 100);
            const deltaL = Math.round(deltas[2] * 100);

            if (deltaH === 0 && deltaS === 0 && deltaL === 0) {
              return;
            }

            for (let i = 0; i < data.length; i += 4) {
              let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];

              // Convert from RGBA to HSL
              let { hue: h, saturation: s, lightness: l } = this.rgbaToHsl(`rgba(${r}, ${g}, ${b}, ${a})`);

              // Apply deltas from SketchUp
              if (colorized) {
                // Colorize ON: Replace Hue with deltaH
                h = deltaH;
                if (h < 0) h += 360;
              } else {
                // Colorize OFF: Add deltaH to original Hue
                h = h + deltaH;
                // Ensure Hue is in range 0-360 (handle negative values)
                h = ((h % 360) + 360) % 360;
              }
              l = Math.max(0, Math.min(100, l + deltaL));
              s = Math.max(0, Math.min(100, s + deltaS));

              // Convert back from HSL to RGB (chia saturation và lightness cho 100)
              let { r: newR, g: newG, b: newB } = this.hslToRgb(h, s, l);

              // Update RGB values for image data
              data[i] = newR;
              data[i + 1] = newG;
              data[i + 2] = newB;
            }

            // Update image on canvas
            ctx.putImageData(imageData, 0, 0);
          }
        }
      };

    },

    rgbaToHsl(rgbaString) {
      const rgbaMatch = rgbaString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*[\d\.]*\)/i);

      if (!rgbaMatch) {
        throw new Error("Invalid RGBA format");
      }

      let r = parseInt(rgbaMatch[1], 10) / 255;
      let g = parseInt(rgbaMatch[2], 10) / 255;
      let b = parseInt(rgbaMatch[3], 10) / 255;

      let max = Math.max(r, g, b), min = Math.min(r, g, b);
      let h, s, l = (max + min) / 2;

      if (max === min) {
        h = s = 0;
      } else {
        let delta = max - min;
        s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
    
        switch (max) {
          case r:
            h = (g - b) / delta + (g < b ? 6 : 0);
            break;
          case g:
            h = (b - r) / delta + 2;
            break;
          case b:
            h = (r - g) / delta + 4;
            break;
        }

        h = Math.round(h * 60);
        if (h < 0) h += 360;
      }

      s = Math.round(s * 100);
      l = Math.round(l * 100);

      return {
        hue: h,
        saturation: s,
        lightness: l,
        hslString: `hsl(${h}, ${s}%, ${l}%)`,
      };
    },

    hslToRgb(h, s, l) {
      s /= 100;
      l /= 100;

      const c = (1 - Math.abs(2 * l - 1)) * s;
      const x = c * (1 - Math.abs((h / 60) % 2 - 1));
      const m = l - c / 2;
      let r = 0, g = 0, b = 0;

      if (h >= 0 && h < 60) {
        r = c; g = x; b = 0;
      } else if (h >= 60 && h < 120) {
        r = x; g = c; b = 0;
      } else if (h >= 120 && h < 180) {
        r = 0; g = c; b = x;
      } else if (h >= 180 && h < 240) {
        r = 0; g = x; b = c;
      } else if (h >= 240 && h < 300) {
        r = x; g = 0; b = c;
      } else if (h >= 300 && h < 360) {
        r = c; g = 0; b = x;
      }

      r = Math.round((r + m) * 255);
      g = Math.round((g + m) * 255);
      b = Math.round((b + m) * 255);

      return { r, g, b };
    },
  },

  template: /*html*/`
    <div
      :class="['d-flex', 'library-container', sidebarOpen ? 'sidebar-open' : 'sidebar-closed']"
      id="material-app"
    >
      <div
        class="sidebar-wrapper"
        :style="{ width: sidebarOpen ? (sidebarWidth + 'px') : '0' }"
      >
        <side-bar
          type="material"
        :isOnline="isOnline"
        :selectedType="selectedType"
        :folders="materialFolders"
        :sortOrderFolders="sortOrderFolders"
        :recentFolderPath="recentFolderPath"
        :selectedFolderPath="selectedFolderPath"
        :treeRootPath="treeRootPath || ''"
        @switch-mode="switchMode"
        @browse-collection="browseCollection"
        @sort-folders="sortFolders"
        @add-collection="addCollection"
        @browse-folder="onLeftBrowseFolder"
        @folder-context-menu="(payload) => showCollectionContextMenu(payload.event, payload.folderPath, payload.folderName)"
        @separator-context-menu="showSeparatorContextMenu"
        @hide-context-menu="hideContextMenu"
        @folder-enter="(payload) => onEnterKey(payload.folderPath, payload.folderName)"
        @folder-blur="(payload) => onBlur(payload.folderPath, payload.folderName)"
        @folder-esc="(payload) => cancelRename(payload.folderPath)"
        ></side-bar>
        <div
          v-show="sidebarOpen"
          class="resize-handle"
          @mousedown.prevent="startResizeSidebar"
          title="拖拽调节宽度"
        ></div>
      </div>

      <div
        v-if="!isOnline && subfolderColumnVisible"
        class="subfolder-column-wrapper"
        :style="{ width: subfolderColumnWidth + 'px' }"
      >
        <subfolder-column
          type="material"
          :visible="true"
          :items="subfolderTreeItems"
          :selected-folder-path="selectedFolderPath"
          @browse-subfolder="(path) => browseFolder(path, false)"
        ></subfolder-column>
        <div
          class="subfolder-resize-handle"
          @mousedown.prevent="startResizeSubfolderColumn"
          title="拖拽调节宽度"
        ></div>
      </div>

      <online-container
        ref="onlineContainer"
        type="material"
        :isOnline="isOnline"
        :defaultSource="defaultSource"
        :onlineSources="onlineSources"
        :currentUrl="currentUrl"
        @browse-url="browseUrl"
        @show-online-context-menu="(payload) => showOnlineContextMenu(payload.event, payload.source)"
        @save-online-source-name="(payload) => saveNewOnlineSourceName(payload.url, payload.oldName)"
        @open-add-source="openAddSource"
      ></online-container>

      <div v-if="!isOnline" class="local-container flex-grow-1 subfolder-main-content">
        <local-panel
          type="material"
          :sidebarOpen="sidebarOpen"
          :isSmallWindow="isSmallWindow"
          :selectedType="selectedType"
          :isDrive="isDrive"
          :showShowEditMaterial="showShowEditMaterial"
          :isMediumWindow="isMediumWindow"
          :viewMode="viewMode"
          :showDetails="showDetails"
          :currentView="currentView"
          :selectedSort="selectedSort"
          :activeFolderPath="activeFolderPath || ''"
          :selectedFolderPath="selectedFolderPath || ''"
          :treeRootPath="treeRootPath || ''"
          @handle-edit-material="editMaterial"
          @handle-parent-click="handleParentClick"
          @browse-folder-path="(path) => browseFolder(path, false)"
          @add-selected="addSelected(selectedFolderPath)"
          @details-change="saveDetailsState"
          @change-view="changeView"
          @update-thumbnails="updateThumbnails"
          @sort-by="sortedBy"
        ></local-panel>

        <material-content
          :isOnline="isOnline"
          :selectedType="selectedType"
          :viewMode="viewMode"
          :currentStyle="currentStyle"
          :folderEmpty="folderEmpty"
          :showDetails="showDetails"
          :sidebarOpen="sidebarOpen"
          :isSmallWindow="isSmallWindow"
          :isMediumWindow="isMediumWindow"
          :filteredItems="filteredItems"
          :selectedMaterials="selectedMaterials"
          :favorites="favorites"
          :overlayStyles="overlayStyles"
          @content-context-menu="showContentContextMenu"
          @item-context-menu="(payload) => showItemContextMenu(payload.event, payload.itemInfo)"
          @item-click="handleItemClick"
          @toggle-favorite="toggleFavorite"
          @quick-action="handleQuickAction"
          @hide-context-menu="hideContextMenu"
          @save-new-item-name="(payload) => saveNewItemName(payload.itemPath, payload.itemName)"
          @cancel-rename-item="(payload) => cancelRenameItem(payload.itemPath, payload.itemName)"
        ></material-content>
      </div>

      <material-context-menus
        :showDetails="showDetails"
        :folderContextMenuVisible="folderContextMenuVisible"
        :folderContextMenuPosition="folderContextMenuPosition"
        :separatorContextMenuVisible="separatorContextMenuVisible"
        :separatorContextMenuPosition="separatorContextMenuPosition"
        :contentContextMenuVisible="contentContextMenuVisible"
        :contentContextMenuPosition="contentContextMenuPosition"
        :selectedType="selectedType"
        :itemContextMenuVisible="itemContextMenuVisible"
        :itemContextMenuPosition="itemContextMenuPosition"
        :isSketchupMaterial="isSketchupMaterial"
        :selectedItemType="selectedItemType"
        :sidebarOpen="sidebarOpen"
        :isMediumWindow="isMediumWindow"
        :onlineContextMenuVisible="onlineContextMenuVisible"
        :onlineContextMenuPosition="onlineContextMenuPosition"
        @collection-actions="collectionActions"
        @content-actions="contentActions"
        @item-actions="itemActions"
        @online-actions="onlineActions"
        @remove-separator="removeSeparator"
      ></material-context-menus>

      <user-options-modal
        type="material"
        :userOptionsMessage="userOptionsMessage"
        :userOptions="userOptions"
      ></user-options-modal>

      <add-source-modal
        type="material"
        @add-source="addSourceOnline"
      ></add-source-modal>
    </div>
  `
};