在React Native鸿蒙跨平台开发中实现一个选择排序算法,如何实现列表项重排序,如任务管理应用中调整任务的优先级

在React Native中实现鸿蒙(HarmonyOS)的原生模块,特别是涉及到原生代码请添加图片描述

js 复制代码
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableNativeArray;

public class SelectionSortModule extends ReactContextBaseJavaModule {
    public SelectionSortModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "SelectionSortModule"; // 模块名必须与JavaScript中保持一致
    }

    @ReactMethod
    public void sortArray(ReadableArray inputArray, Callback callback) {
        int[] array = new int[inputArray.size()];
        for (int i = 0; i < inputArray.size(); i++) {
            array[i] = inputArray.getInt(i);
        }
        selectionSort(array);
        WritableArray outputArray = new WritableNativeArray();
        for (int num : array) {
            outputArray.pushInt(num);
        }
        callback.invoke(outputArray); // 返回排序后的数组到JS
    }

    private void selectionSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // Swap the found minimum element with the first element
            int temp = arr[minIndex];
            arr[minIndex] = arr[i];
            arr[i] = temp;
        }
    }
}

2.2 注册Native Module

MainApplication.java中注册你的模块:

java 复制代码
// MainApplication.java
import harmony.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.myharmonyapp.BuildConfig; // 根据你的包名调整导入路径
import com.myharmonyapp.SelectionSortModule; // 确保路径正确
import java.util.Arrays;
import java.util.List; // 导入List接口用于下面的方法定义中返回一个List<ReactPackage>对象。

public class MainApplication extends Application implements ReactApplication {
  //...其他代码...
  @Override
  protected List<ReactPackage> getPackages() {
    @SuppressWarnings("UnnecessaryLocalVariable") // 为了兼容性检查而保留此变量声明。实际上,我们不需要它。但为了保持一致性,我们保留它。在最新的React Native版本中,可以直接返回一个数组。例如:return Arrays.<ReactPackage>asList(new MainReactPackage(), new SelectionSortModule()); // 注意这里的写法取决于你的React Native版本和配置。在某些情况下,你可能需要使用不同的方式来注册模块。例如,在某些版本的React Native中,你可以直接在getPackages方法中返回一个数组,如下所示:return Arrays.<ReactPackage>asList(new MainReactPackage(), new SelectionSortModulePackage()); 其中SelectionSortModulePackage是一个实现了ReactPackage接口的

真实案例演示代码效果:

js 复制代码
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image } from 'react-native';

// Simple Icon Component using Unicode symbols
interface IconProps {
  name: string;
  size?: number;
  color?: string;
  style?: object;
}

const Icon: React.FC<IconProps> = ({ 
  name, 
  size = 24, 
  color = '#333333',
  style 
}) => {
  const getIconSymbol = () => {
    switch (name) {
      case 'prev': return '‹';
      case 'next': return '›';
      case 'today': return '◎';
      case 'event': return '•';
      case 'date': return '📅';
      case 'month': return '📅';
      case 'year': return '📅';
      default: return '●';
    }
  };

  return (
    <View style={[{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }, style]}>
      <Text style={{ fontSize: size * 0.8, color, includeFontPadding: false, textAlign: 'center' }}>
        {getIconSymbol()}
      </Text>
    </View>
  );
};

// Calendar Component
interface CalendarProps {
  events?: { date: string; title: string }[];
  onDateSelect?: (date: string) => void;
  selectedDate?: string;
}

const Calendar: React.FC<CalendarProps> = ({ 
  events = [], 
  onDateSelect,
  selectedDate 
}) => {
  const [currentDate, setCurrentDate] = useState(new Date());
  
  // Get days in month
  const getDaysInMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate();
  };
  
  // Get first day of month (0 = Sunday, 1 = Monday, etc)
  const getFirstDayOfMonth = (year: number, month: number) => {
    return new Date(year, month, 1).getDay();
  };
  
  // Format date as YYYY-MM-DD
  const formatDate = (date: Date) => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  };
  
  // Check if date has events
  const hasEvents = (dateStr: string) => {
    return events.some(event => event.date === dateStr);
  };
  
  // Navigate to previous month
  const prevMonth = () => {
    setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
  };
  
  // Navigate to next month
  const nextMonth = () => {
    setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
  };
  
  // Go to today
  const goToToday = () => {
    const today = new Date();
    setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1));
    if (onDateSelect) {
      onDateSelect(formatDate(today));
    }
  };
  
  // Render calendar header
  const renderHeader = () => {
    const year = currentDate.getFullYear();
    const month = currentDate.toLocaleString('zh-CN', { month: 'long' });
    
    return (
      <View style={styles.calendarHeader}>
        <TouchableOpacity onPress={prevMonth} style={styles.navButton}>
          <Icon name="prev" size={24} color="#4a5568" />
        </TouchableOpacity>
        
        <View style={styles.monthYearContainer}>
          <Text style={styles.monthText}>{month}</Text>
          <Text style={styles.yearText}>{year}年</Text>
        </View>
        
        <TouchableOpacity onPress={nextMonth} style={styles.navButton}>
          <Icon name="next" size={24} color="#4a5568" />
        </TouchableOpacity>
      </View>
    );
  };
  
  // Render weekdays
  const renderWeekdays = () => {
    const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
    
    return (
      <View style={styles.weekdaysContainer}>
        {weekdays.map((day, index) => (
          <View key={index} style={styles.weekdayCell}>
            <Text style={[styles.weekdayText, index === 0 && styles.sundayText]}>{day}</Text>
          </View>
        ))}
      </View>
    );
  };
  
  // Render calendar days
  const renderDays = () => {
    const year = currentDate.getFullYear();
    const month = currentDate.getMonth();
    const daysInMonth = getDaysInMonth(year, month);
    const firstDayOfMonth = getFirstDayOfMonth(year, month);
    
    const today = new Date();
    const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
    const todayDate = today.getDate();
    
    const days = [];
    
    // Previous month's days
    const prevMonthDays = getDaysInMonth(year, month - 1);
    for (let i = firstDayOfMonth - 1; i >= 0; i--) {
      const day = prevMonthDays - i;
      days.push(
        <View key={`prev-${day}`} style={styles.dayCell}>
          <Text style={styles.otherMonthDay}>{day}</Text>
        </View>
      );
    }
    
    // Current month's days
    for (let day = 1; day <= daysInMonth; day++) {
      const dateObj = new Date(year, month, day);
      const dateStr = formatDate(dateObj);
      const isToday = isCurrentMonth && day === todayDate;
      const isSelected = selectedDate === dateStr;
      const hasEvent = hasEvents(dateStr);
      
      days.push(
        <TouchableOpacity 
          key={`curr-${day}`} 
          style={[
            styles.dayCell,
            isToday && styles.todayCell,
            isSelected && styles.selectedCell
          ]}
          onPress={() => onDateSelect && onDateSelect(dateStr)}
        >
          <View style={styles.dayNumberContainer}>
            <Text style={[
              styles.dayNumber,
              isToday && styles.todayText,
              isSelected && styles.selectedText
            ]}>
              {day}
            </Text>
            {hasEvent && (
              <View style={styles.eventIndicator}>
                <Icon name="event" size={8} color="#e53e3e" />
              </View>
            )}
          </View>
        </TouchableOpacity>
      );
    }
    
    // Next month's days
    const totalCells = 42; // 6 rows * 7 days
    const remainingCells = totalCells - days.length;
    for (let day = 1; day <= remainingCells; day++) {
      days.push(
        <View key={`next-${day}`} style={styles.dayCell}>
          <Text style={styles.otherMonthDay}>{day}</Text>
        </View>
      );
    }
    
    return (
      <View style={styles.daysContainer}>
        {days}
      </View>
    );
  };
  
  // Render today button
  const renderTodayButton = () => {
    return (
      <TouchableOpacity style={styles.todayButton} onPress={goToToday}>
        <Icon name="today" size={16} color="#4a5568" style={styles.todayIcon} />
        <Text style={styles.todayButtonText}>今天</Text>
      </TouchableOpacity>
    );
  };

  return (
    <View style={styles.calendarContainer}>
      {renderHeader()}
      {renderWeekdays()}
      {renderDays()}
      {renderTodayButton()}
    </View>
  );
};

// Main App Component
const CalendarComponentApp = () => {
  const [selectedDate, setSelectedDate] = useState<string>('');
  const [events] = useState([
    { date: new Date().toISOString().split('T')[0], title: '会议' },
    { date: new Date(Date.now() + 86400000).toISOString().split('T')[0], title: '生日' },
    { date: new Date(Date.now() + 172800000).toISOString().split('T')[0], title: '假期' },
  ]);

  const handleDateSelect = (date: string) => {
    setSelectedDate(date);
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>选择排序组件</Text>
        <Text style={styles.headerSubtitle}>逐步演示选择排序</Text>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>选择排序(可视化 · 海洋风格)</Text>
        <View style={styles.ssContainer}>
          <SelectionSortVisualizer />
        </View>
      </View>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 排序演示 | 现代化UI组件库</Text>
      </View>
    </ScrollView>
  );
};

const { width } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f7fafc',
  },
  header: {
    backgroundColor: '#ffffff',
    paddingVertical: 30,
    paddingHorizontal: 20,
    marginBottom: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: '700',
    color: '#2d3748',
    textAlign: 'center',
    marginBottom: 5,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#718096',
    textAlign: 'center',
  },
  section: {
    marginBottom: 25,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: '700',
    color: '#2d3748',
    paddingHorizontal: 20,
    paddingBottom: 15,
  },
  calendarWrapper: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 15,
    elevation: 4,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
  },
  selectedDateCard: {
    backgroundColor: '#ebf8ff',
    marginHorizontal: 15,
    borderRadius: 12,
    padding: 20,
    borderWidth: 1,
    borderColor: '#bee3f8',
  },
  selectedDateText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2b6cb0',
    textAlign: 'center',
    marginBottom: 5,
  },
  selectedDateDesc: {
    fontSize: 14,
    color: '#4a5568',
    textAlign: 'center',
  },
  demosContainer: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  demoItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 20,
  },
  demoItemLast: {
    marginBottom: 0,
  },
  demoIcon: {
    marginRight: 15,
  },
  demoTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#2d3748',
    marginBottom: 3,
  },
  demoDesc: {
    fontSize: 14,
    color: '#718096',
  },
  usageSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  codeBlock: {
    backgroundColor: '#2d3748',
    borderRadius: 8,
    padding: 15,
    marginBottom: 15,
  },
  codeText: {
    fontFamily: 'monospace',
    color: '#e2e8f0',
    fontSize: 14,
    lineHeight: 22,
  },
  description: {
    fontSize: 15,
    color: '#4a5568',
    lineHeight: 22,
  },
  featuresSection: {
    backgroundColor: '#ffffff',
    marginHorizontal: 15,
    borderRadius: 15,
    padding: 20,
    marginBottom: 20,
    elevation: 3,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
  },
  featuresList: {
    paddingLeft: 10,
  },
  featureItem: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 15,
  },
  featureIcon: {
    marginRight: 15,
  },
  featureText: {
    fontSize: 16,
    color: '#2d3748',
  },
  footer: {
    paddingVertical: 20,
    alignItems: 'center',
  },
  footerText: {
    color: '#a0aec0',
    fontSize: 14,
  },
  // Calendar Styles
  calendarContainer: {
    backgroundColor: '#ffffff',
  },
  calendarHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 10,
    paddingVertical: 15,
  },
  navButton: {
    padding: 10,
  },
  monthYearContainer: {
    alignItems: 'center',
  },
  monthText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2d3748',
  },
  yearText: {
    fontSize: 14,
    color: '#718096',
  },
  weekdaysContainer: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#edf2f7',
    paddingBottom: 10,
    marginBottom: 5,
  },
  weekdayCell: {
    flex: 1,
    alignItems: 'center',
  },
  weekdayText: {
    fontSize: 14,
    fontWeight: '600',
    color: '#4a5568',
  },
  sundayText: {
    color: '#e53e3e',
  },
  daysContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  dayCell: {
    width: '14.28%',
    aspectRatio: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  dayNumberContainer: {
    position: 'relative',
  },
  dayNumber: {
    fontSize: 16,
    color: '#4a5568',
    fontWeight: '500',
  },
  otherMonthDay: {
    fontSize: 16,
    color: '#cbd5e0',
  },
  todayCell: {
    backgroundColor: '#ebf8ff',
    borderRadius: 30,
  },
  todayText: {
    color: '#3182ce',
    fontWeight: '700',
  },
  selectedCell: {
    backgroundColor: '#3182ce',
    borderRadius: 30,
  },
  selectedText: {
    color: '#ffffff',
    fontWeight: '700',
  },
  eventIndicator: {
    position: 'absolute',
    top: -3,
    right: -3,
  },
  todayButton: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 15,
    borderTopWidth: 1,
    borderTopColor: '#edf2f7',
    marginTop: 5,
  },
  todayIcon: {
    marginRight: 8,
  },
  todayButtonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#4a5568',
  },
  msContainer: {
    marginHorizontal: 15,
  },
  msCard: {
    backgroundColor: '#0f141c',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#1f2a36',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  msHeader: {
    marginBottom: 8,
  },
  msTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#e6f1ff',
  },
  msSubtitle: {
    fontSize: 13,
    color: '#8aa0b8',
    marginTop: 4,
  },
  msControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  msBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#1a2433',
    borderWidth: 1,
    borderColor: '#28374d',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  msBtnActive: {
    backgroundColor: '#1f2d44',
    borderColor: '#34507a',
  },
  msIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  msBtnText: {
    color: '#e6f1ff',
    fontSize: 13,
    fontWeight: '600',
  },
  msChips: {
    flexDirection: 'row',
    alignSelf: 'flex-end',
    marginLeft: 8,
  },
  msChip: {
    backgroundColor: '#182030',
    borderWidth: 1,
    borderColor: '#26364d',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  msChipActive: {
    backgroundColor: '#213049',
    borderColor: '#385782',
  },
  msChipText: {
    color: '#cfe1ff',
    fontSize: 12,
    fontWeight: '600',
  },
  msBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  msBar: {
    width: 16,
    backgroundColor: '#2f7de1',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  msBarHL: {
    backgroundColor: '#49a2ff',
  },
  msBarText: {
    fontSize: 10,
    color: '#e6f1ff',
    marginBottom: 4,
    fontWeight: '600',
  },
  msFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  msFootText: {
    color: '#8aa0b8',
    fontSize: 12,
  },
  msDone: {
    color: '#43d998',
    fontWeight: '700',
  },
  bsContainer: {
    marginHorizontal: 15,
  },
  bsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#e7eef7',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.08,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bsHeader: {
    marginBottom: 8,
  },
  bsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#2d3748',
  },
  bsSubtitle: {
    fontSize: 13,
    color: '#718096',
    marginTop: 4,
  },
  bsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#f1f5fb',
    borderWidth: 1,
    borderColor: '#e1eaf6',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bsBtnActive: {
    backgroundColor: '#e7eef7',
    borderColor: '#d9e6f5',
  },
  bsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bsBtnText: {
    color: '#2d3748',
    fontSize: 13,
    fontWeight: '600',
  },
  bsTargetRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
  },
  bsChip: {
    backgroundColor: '#edf2f7',
    borderWidth: 1,
    borderColor: '#e2e8f0',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bsChipText: {
    color: '#2d3748',
    fontSize: 12,
    fontWeight: '600',
  },
  bsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  bsBar: {
    width: 16,
    backgroundColor: '#90cdf4',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  bsBarDim: {
    backgroundColor: '#c5d8ee',
  },
  bsBarMid: {
    backgroundColor: '#63b3ed',
  },
  bsBarTarget: {
    backgroundColor: '#68d391',
  },
  bsBarText: {
    fontSize: 10,
    color: '#2d3748',
    marginBottom: 4,
    fontWeight: '600',
  },
  bsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bsFootText: {
    color: '#718096',
    fontSize: 12,
  },
  bsDone: {
    color: '#2e7d32',
    fontWeight: '700',
  },
  lsContainer: {
    marginHorizontal: 15,
  },
  lsCard: {
    backgroundColor: '#fff7ed',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#fde4d6',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.08,
    shadowRadius: 12,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  lsHeader: {
    marginBottom: 8,
  },
  lsTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#7c2d12',
  },
  lsSubtitle: {
    fontSize: 13,
    color: '#9a3412',
    marginTop: 4,
  },
  lsControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  lsBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff1e6',
    borderWidth: 1,
    borderColor: '#ffe1cc',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  lsBtnActive: {
    backgroundColor: '#ffe8d6',
    borderColor: '#ffd9bf',
  },
  lsIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  lsBtnText: {
    color: '#7c2d12',
    fontSize: 13,
    fontWeight: '600',
  },
  lsTargetRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
  },
  lsChip: {
    backgroundColor: '#ffedd5',
    borderWidth: 1,
    borderColor: '#fed7aa',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  lsChipText: {
    color: '#7c2d12',
    fontSize: 12,
    fontWeight: '600',
  },
  lsBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  lsBar: {
    width: 16,
    backgroundColor: '#fdba74',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  lsBarCur: {
    backgroundColor: '#fb923c',
  },
  lsBarTarget: {
    backgroundColor: '#22c55e',
  },
  lsBarText: {
    fontSize: 10,
    color: '#7c2d12',
    marginBottom: 4,
    fontWeight: '600',
  },
  lsFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  lsFootText: {
    color: '#9a3412',
    fontSize: 12,
  },
  lsDone: {
    color: '#16a34a',
    fontWeight: '700',
  },
  bbContainer: {
    marginHorizontal: 15,
  },
  bbCard: {
    backgroundColor: '#1B1633',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#2A2350',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.2,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bbHeader: {
    marginBottom: 8,
  },
  bbTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#EAE7FF',
  },
  bbSubtitle: {
    fontSize: 13,
    color: '#B9B4E6',
    marginTop: 4,
  },
  bbControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bbBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#231C44',
    borderWidth: 1,
    borderColor: '#3A2F6D',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bbBtnActive: {
    backgroundColor: '#2B2356',
    borderColor: '#4C3F8F',
  },
  bbIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bbBtnText: {
    color: '#EAE7FF',
    fontSize: 13,
    fontWeight: '600',
  },
  bbChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  bbChip: {
    backgroundColor: '#211B3F',
    borderWidth: 1,
    borderColor: '#372F6B',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bbChipActive: {
    backgroundColor: '#2A2350',
    borderColor: '#4D3F91',
  },
  bbChipText: {
    color: '#DCD7FF',
    fontSize: 12,
    fontWeight: '600',
  },
  bbBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  bbBar: {
    width: 16,
    backgroundColor: '#7C3AED',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  bbBarHL: {
    backgroundColor: '#A78BFA',
  },
  bbBarSwap: {
    backgroundColor: '#F87171',
  },
  bbBarSorted: {
    backgroundColor: '#6B7280',
  },
  bbBarText: {
    fontSize: 10,
    color: '#EAE7FF',
    marginBottom: 4,
    fontWeight: '600',
  },
  bbFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bbFootText: {
    color: '#B9B4E6',
    fontSize: 12,
  },
  bbDone: {
    color: '#34D399',
    fontWeight: '700',
  },
  ssContainer: {
    marginHorizontal: 15,
  },
  ssCard: {
    backgroundColor: '#07242D',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#0D3A46',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  ssHeader: {
    marginBottom: 8,
  },
  ssTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#DFF6FF',
  },
  ssSubtitle: {
    fontSize: 13,
    color: '#96C9D6',
    marginTop: 4,
  },
  ssControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  ssBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#0B2E37',
    borderWidth: 1,
    borderColor: '#134753',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  ssBtnActive: {
    backgroundColor: '#0E3B46',
    borderColor: '#1B5C6A',
  },
  ssIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  ssBtnText: {
    color: '#DFF6FF',
    fontSize: 13,
    fontWeight: '600',
  },
  ssChips: {
    flexDirection: 'row',
    marginLeft: 8,
  },
  ssChip: {
    backgroundColor: '#0A2B35',
    borderWidth: 1,
    borderColor: '#134753',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  ssChipActive: {
    backgroundColor: '#0E3B46',
    borderColor: '#1B5C6A',
  },
  ssChipText: {
    color: '#CDEBF4',
    fontSize: 12,
    fontWeight: '600',
  },
  ssBars: {
    flexDirection: 'row',
    alignItems: 'flex-end',
    justifyContent: 'space-between',
    paddingVertical: 12,
    paddingHorizontal: 6,
    minHeight: 220,
  },
  ssBar: {
    width: 16,
    backgroundColor: '#22D3EE',
    borderTopLeftRadius: 8,
    borderTopRightRadius: 8,
    borderBottomLeftRadius: 2,
    borderBottomRightRadius: 2,
    justifyContent: 'flex-end',
    alignItems: 'center',
    marginHorizontal: 2,
  },
  ssBarHL: {
    backgroundColor: '#38BDF8',
  },
  ssBarMin: {
    backgroundColor: '#2DD4BF',
  },
  ssBarSwap: {
    backgroundColor: '#F59E0B',
  },
  ssBarSorted: {
    backgroundColor: '#6B7280',
  },
  ssBarText: {
    fontSize: 10,
    color: '#DFF6FF',
    marginBottom: 4,
    fontWeight: '600',
  },
  ssFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  ssFootText: {
    color: '#96C9D6',
    fontSize: 12,
  },
  ssDone: {
    color: '#34D399',
    fontWeight: '700',
  },
  bstContainer: {
    marginHorizontal: 15,
  },
  bstCard: {
    backgroundColor: '#0b1f16',
    borderRadius: 16,
    borderWidth: 1,
    borderColor: '#163529',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 6 },
    shadowOpacity: 0.18,
    shadowRadius: 14,
    paddingVertical: 14,
    paddingHorizontal: 12,
  },
  bstHeader: {
    marginBottom: 8,
  },
  bstTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#e7f6ef',
  },
  bstSubtitle: {
    fontSize: 13,
    color: '#a6c9bb',
    marginTop: 4,
  },
  bstControls: {
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    marginTop: 8,
    marginBottom: 12,
  },
  bstBtn: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#123426',
    borderWidth: 1,
    borderColor: '#1a4a37',
    borderRadius: 12,
    paddingVertical: 8,
    paddingHorizontal: 10,
    marginRight: 8,
  },
  bstBtnActive: {
    backgroundColor: '#154234',
    borderColor: '#22624a',
  },
  bstIcon: {
    width: 20,
    height: 20,
    marginRight: 6,
  },
  bstBtnText: {
    color: '#e7f6ef',
    fontSize: 13,
    fontWeight: '600',
  },
  bstChip: {
    backgroundColor: '#0f2c20',
    borderWidth: 1,
    borderColor: '#1a4a37',
    borderRadius: 10,
    paddingVertical: 6,
    paddingHorizontal: 10,
    marginLeft: 6,
  },
  bstChipText: {
    color: '#cde7dc',
    fontSize: 12,
    fontWeight: '600',
  },
  bstTree: {
    position: 'relative',
    minHeight: 420,
    backgroundColor: '#0e251c',
    borderRadius: 12,
    borderWidth: 1,
    borderColor: '#153a2c',
    padding: 8,
  },
  bstNode: {
    position: 'absolute',
    width: 40,
    height: 40,
    borderRadius: 20,
    backgroundColor: '#1f6f4d',
    borderWidth: 2,
    borderColor: '#2e8a64',
    justifyContent: 'center',
    alignItems: 'center',
  },
  bstNodeHL: {
    backgroundColor: '#239f6b',
    borderColor: '#3ac18b',
  },
  bstNodeFound: {
    backgroundColor: '#1fbf72',
    borderColor: '#57d69b',
  },
  bstNodeText: {
    color: '#e7f6ef',
    fontSize: 12,
    fontWeight: '700',
  },
  bstLine: {
    position: 'absolute',
    backgroundColor: '#1a4a37',
  },
  bstFooterRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 6,
  },
  bstFootText: {
    color: '#a6c9bb',
    fontSize: 12,
  },
  bstDone: {
    color: '#43d998',
    fontWeight: '700',
  },
});

const ICON_BASE64_MS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BST = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  insert: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  remove: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  find: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BUBBLE = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_SELECT = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_BS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABN0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const ICON_BASE64_LS = {
  play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABJUlEQVR4nO2Uu0oDQRRGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABK0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  step: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  reset: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABMklEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
  shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsSAAALEgHS3X78AAABO0lEQVR4nO2UsUoDQRSGv6WgQbYgG5gMZAGyQJgBskCYAbLA2QBsEB2YjI9kqg+oJrJf6Vb4eU2m3mS9KcZkqgqj3v8f4cIhJ0c3xQkQyGv4gqJtVZr3kzF1g2cXgH0mGJ8xwHj0C3kC4g4S9gJ4g4p7UeUuJ5ZJrF7JwE7k8Kf4mA1Jwu0SxwV6pJm1fVb9kqUoSxG1zQzqg3bqB9oC+o9dC7kP2kWmA6lH5wZkQ7Wc1wV2oG+oS6gI4gU6gC6gN5gH5gM5gG5gN6gC4gE6gE6gP7i9n3yqgW+uQmHn3Rz2bQf3pKf8dC6lHkU1YcH5tQ5ZkZyLkGm2r9wUOJi3Yq6Ww1b4q3bWw5b8m8YHc2mXb9jZVNcWwA8Yw3fD6oGJcQbJcXIqk3wqkP1pZ3yqgV8wQf4pXQm+YH3kR+gKpUuZQZVYVq0gGv8nqfYw3gP2gD8oG6gE8kD0kP4kD0oP2kH8oD+oJ8oP+oH8kD+oH8oP9qgWgB7vKc3I2q2xAAAAAElFTkSuQmCC',
};

const MergeSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; highlights: number[] }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[]) => {
    const n = a.length;
    let src = a.slice();
    const out: { arr: number[]; highlights: number[] }[] = [{ arr: src.slice(), highlights: [] }];
    for (let size = 1; size < n; size *= 2) {
      for (let left = 0; left < n; left += 2 * size) {
        const mid = Math.min(left + size, n);
        const right = Math.min(left + 2 * size, n);
        let i = left, j = mid;
        const merged: number[] = [];
        while (i < mid && j < right) {
          if (src[i] <= src[j]) { merged.push(src[i]); i++; } else { merged.push(src[j]); j++; }
        }
        while (i < mid) { merged.push(src[i]); i++; }
        while (j < right) { merged.push(src[j]); j++; }
        for (let k = 0; k < merged.length; k++) src[left + k] = merged[k];
        out.push({ arr: src.slice(), highlights: Array.from({ length: merged.length }, (_, k) => left + k) });
      }
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.msCard}>
      <View style={styles.msHeader}> 
        <Text style={styles.msTitle}>归并排序 · 霓虹风格</Text>
        <Text style={styles.msSubtitle}>播放、步进、重置、随机</Text>
      </View>
      <View style={styles.msControls}>
        <TouchableOpacity style={[styles.msBtn, playing ? styles.msBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_MS.pause : ICON_BASE64_MS.play }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.step }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.reset }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.msBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_MS.shuffle }} style={styles.msIcon} />
          <Text style={styles.msBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.msChips}>
          <TouchableOpacity style={styles.msChip} onPress={() => setSpeed(900)}><Text style={styles.msChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.msChip, styles.msChipActive]} onPress={() => setSpeed(600)}><Text style={styles.msChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.msChip} onPress={() => setSpeed(300)}><Text style={styles.msChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.msBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const hl = steps[idx]?.highlights.includes(i);
          return (
            <View key={i} style={[styles.msBar, { height: h }, hl ? styles.msBarHL : null]}>
              <Text style={styles.msBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.msFooterRow}>
        <Text style={styles.msFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.msFootText, isDone ? styles.msDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};
const BinarySearchVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<{ low: number; high: number; mid: number; found: boolean }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const maxBars = 20;

  const genArray = () => {
    const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
    const sorted = base.sort((a, b) => a - b);
    setArr(sorted);
    const t = sorted[Math.floor(Math.random() * sorted.length)];
    setTarget(t);
    const s = buildSteps(sorted, t);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[], t: number) => {
    let low = 0, high = a.length - 1;
    const out: { low: number; high: number; mid: number; found: boolean }[] = [];
    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      const found = a[mid] === t;
      out.push({ low, high, mid, found });
      if (found) break;
      if (a[mid] < t) low = mid + 1; else high = mid - 1;
    }
    if (out.length === 0) out.push({ low: 0, high: a.length - 1, mid: -1, found: false });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 0;
  const cur = steps[idx] || { low: 0, high: arr.length - 1, mid: -1, found: false };

  return (
    <View style={styles.bsCard}>
      <View style={styles.bsHeader}> 
        <Text style={styles.bsTitle}>二分查找 · 柔和风格</Text>
        <Text style={styles.bsSubtitle}>播放、步进、重置、随机、目标调整</Text>
      </View>
      <View style={styles.bsControls}>
        <TouchableOpacity style={[styles.bsBtn, playing ? styles.bsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BS.pause : ICON_BASE64_BS.play }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.step }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.reset }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BS.shuffle }} style={styles.bsIcon} />
          <Text style={styles.bsBtnText}>随机</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.bsTargetRow}>
        <TouchableOpacity style={styles.bsChip} onPress={decTarget}><Text style={styles.bsChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.bsChip}><Text style={styles.bsChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.bsChip} onPress={incTarget}><Text style={styles.bsChipText}>目标 +</Text></TouchableOpacity>
        <View style={styles.bsChip}><Text style={styles.bsChipText}>范围:{cur.low} - {cur.high}</Text></View>
      </View>
      <View style={styles.bsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inRange = i >= cur.low && i <= cur.high;
          const isMid = i === cur.mid;
          const isTarget = v === target && inRange;
          return (
            <View key={i} style={[styles.bsBar, { height: h }, !inRange ? styles.bsBarDim : null, isMid ? styles.bsBarMid : null, isTarget ? styles.bsBarTarget : null]}>
              <Text style={styles.bsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.bsFooterRow}>
        <Text style={styles.bsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.bsFootText, isDone && cur.found ? styles.bsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
      </View>
    </View>
  );
};
const LinearSearchVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<{ idx: number; found: boolean }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const maxBars = 22;

  const genArray = () => {
    const base = Array.from({ length: maxBars }, () => Math.floor(10 + Math.random() * 150));
    setArr(base);
    const t = base[Math.floor(Math.random() * base.length)];
    setTarget(t);
    const s = buildSteps(base, t);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (a: number[], t: number) => {
    const out: { idx: number; found: boolean }[] = [];
    for (let i = 0; i < a.length; i++) {
      const found = a[i] === t;
      out.push({ idx: i, found });
      if (found) break;
    }
    if (out.length === 0) out.push({ idx: -1, found: false });
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const s = buildSteps(arr.slice(), target); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();
  const incTarget = () => setTarget(t => { const nt = t + 1; const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const s = buildSteps(arr.slice(), nt); setSteps(s); setIdx(0); return nt; });

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const isDone = idx === steps.length - 1 && steps.length > 0;
  const cur = steps[idx] || { idx: -1, found: false };

  return (
    <View style={styles.lsCard}>
      <View style={styles.lsHeader}> 
        <Text style={styles.lsTitle}>线性查找 · 暖色风格</Text>
        <Text style={styles.lsSubtitle}>播放、步进、重置、随机、目标调整</Text>
      </View>
      <View style={styles.lsControls}>
        <TouchableOpacity style={[styles.lsBtn, playing ? styles.lsBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_LS.pause : ICON_BASE64_LS.play }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.step }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.reset }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.lsBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_LS.shuffle }} style={styles.lsIcon} />
          <Text style={styles.lsBtnText}>随机</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.lsTargetRow}>
        <TouchableOpacity style={styles.lsChip} onPress={decTarget}><Text style={styles.lsChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.lsChip}><Text style={styles.lsChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.lsChip} onPress={incTarget}><Text style={styles.lsChipText}>目标 +</Text></TouchableOpacity>
        <View style={styles.lsChip}><Text style={styles.lsChipText}>索引:{cur.idx >= 0 ? cur.idx : '-'}</Text></View>
      </View>
      <View style={styles.lsBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const isCur = i === cur.idx;
          const isTarget = v === target;
          return (
            <View key={i} style={[styles.lsBar, { height: h }, isCur ? styles.lsBarCur : null, isTarget ? styles.lsBarTarget : null]}>
              <Text style={styles.lsBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.lsFooterRow}>
        <Text style={styles.lsFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.lsFootText, isDone && cur.found ? styles.lsDone : null]}>{isDone ? (cur.found ? '已找到目标' : '未找到') : '进行中'}</Text>
      </View>
    </View>
  );
};
const BSTVisualizer: React.FC = () => {
  const [root, setRoot] = useState<any>(null);
  const [target, setTarget] = useState<number>(0);
  const [steps, setSteps] = useState<number[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(700);
  const timerRef = React.useRef<any>(null);
  const levelGap = 90;
  const padding = 24;

  const insert = (k: number) => {
    const node = { key: k, left: null, right: null };
    if (!root) { setRoot(node); return; }
    const r = JSON.parse(JSON.stringify(root));
    let cur: any = r;
    while (true) {
      if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
      else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
      else break;
    }
    setRoot(r);
  };

  const findPath = (k: number) => {
    const path: number[] = [];
    let cur: any = root;
    while (cur) {
      path.push(cur.key);
      if (k === cur.key) break;
      cur = k < cur.key ? cur.left : cur.right;
    }
    return path;
  };

  const minNode = (n: any) => { let c = n; while (c && c.left) c = c.left; return c; };
  const removeKey = (k: number) => {
    const del = (n: any, v: number): any => {
      if (!n) return null;
      if (v < n.key) { n.left = del(n.left, v); return n; }
      if (v > n.key) { n.right = del(n.right, v); return n; }
      if (!n.left) return n.right;
      if (!n.right) return n.left;
      const succ = minNode(n.right);
      n.key = succ.key;
      n.right = del(n.right, succ.key);
      return n;
    };
    const r = JSON.parse(JSON.stringify(root));
    setRoot(del(r, k));
  };

  const buildRandom = () => {
    let r: any = null;
    const values = Array.from({ length: 10 }, () => Math.floor(10 + Math.random() * 90));
    values.forEach(v => { r = insertInto(r, v); });
    setRoot(r);
    const t = values[Math.floor(Math.random() * values.length)];
    setTarget(t);
    const p = findPathOn(r, t);
    setSteps(p);
    setIdx(0);
    setPlaying(false);
  };

  const insertInto = (r: any, k: number) => {
    const node = { key: k, left: null, right: null };
    if (!r) return node;
    let cur = r;
    while (true) {
      if (k < cur.key) { if (cur.left) cur = cur.left; else { cur.left = node; break; } }
      else if (k > cur.key) { if (cur.right) cur = cur.right; else { cur.right = node; break; } }
      else break;
    }
    return r;
  };

  const findPathOn = (r: any, k: number) => {
    const path: number[] = [];
    let cur = r;
    while (cur) {
      path.push(cur.key);
      if (k === cur.key) break;
      cur = k < cur.key ? cur.left : cur.right;
    }
    return path;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSearch = () => { const p = findPath(target); setSteps(p); setIdx(0); setPlaying(false); };
  const shuffle = () => buildRandom();
  const incTarget = () => setTarget(t => { const nt = t + 1; const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
  const decTarget = () => setTarget(t => { const nt = Math.max(0, t - 1); const p = findPath(nt); setSteps(p); setIdx(0); return nt; });
  const doInsert = () => { const v = Math.floor(10 + Math.random() * 90); insert(v); const p = findPath(target); setSteps(p); setIdx(0); };
  const doRemove = () => { removeKey(target); const p = findPath(target); setSteps(p); setIdx(0); };
  const clearTree = () => { setRoot(null); setSteps([]); setIdx(0); setPlaying(false); };

  React.useEffect(() => { buildRandom(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const layout = buildLayout(root);
  const curKey = steps[idx];
  const isDone = steps.length > 0 && idx === steps.length - 1 && curKey === target;

  return (
    <View style={styles.bstCard}>
      <View style={styles.bstHeader}> 
        <Text style={styles.bstTitle}>二叉搜索树 · 森林风格</Text>
        <Text style={styles.bstSubtitle}>播放、步进、插入、删除、目标调整</Text>
      </View>
      <View style={styles.bstControls}>
        <TouchableOpacity style={[styles.bstBtn, playing ? styles.bstBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BST.pause : ICON_BASE64_BST.play }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.step }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={resetSearch} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.reset }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.shuffle }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>随机</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={doInsert} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.insert }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>插入</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bstBtn} onPress={doRemove} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BST.remove }} style={styles.bstIcon} />
          <Text style={styles.bstBtnText}>删除</Text>
        </TouchableOpacity>
      </View>
      <View style={styles.bstControls}>
        <TouchableOpacity style={styles.bstChip} onPress={decTarget}><Text style={styles.bstChipText}>目标 -</Text></TouchableOpacity>
        <View style={styles.bstChip}><Text style={styles.bstChipText}>当前目标:{target}</Text></View>
        <TouchableOpacity style={styles.bstChip} onPress={incTarget}><Text style={styles.bstChipText}>目标 +</Text></TouchableOpacity>
      </View>
      <View style={styles.bstTree}>
        {layout.lines.map((l: any, i: number) => (
          <View key={`line-${i}`} style={[styles.bstLine, { left: l.vx, top: l.vy, width: 2, height: l.vh }]} />
        ))}
        {layout.lines.map((l: any, i: number) => (
          <View key={`hline-${i}`} style={[styles.bstLine, { left: l.hx, top: l.hy, width: l.hw, height: 2 }]} />
        ))}
        {layout.nodes.map((n: any) => (
          <View key={`node-${n.key}-${n.x}-${n.y}`} style={[styles.bstNode, { left: n.x - 20, top: n.y - 20 }, n.key === curKey ? styles.bstNodeHL : null, n.key === target && curKey === target ? styles.bstNodeFound : null]}>
            <Text style={styles.bstNodeText}>{n.key}</Text>
          </View>
        ))}
      </View>
      <View style={styles.bstFooterRow}>
        <Text style={styles.bstFootText}>步骤 {idx + 1} / {Math.max(steps.length, 0)}</Text>
        <Text style={[styles.bstFootText, isDone ? styles.bstDone : null]}>{isDone ? '已找到目标' : '进行中'}</Text>
      </View>
    </View>
  );

  function buildLayout(r: any) {
    const nodes: any[] = [];
    const lines: any[] = [];
    if (!r) return { nodes, lines };
    const queue: any[] = [{ n: r, level: 0, idx: 0 }];
    const levels: any = {};
    while (queue.length) {
      const item = queue.shift();
      const lvl = item.level;
      levels[lvl] = levels[lvl] || [];
      levels[lvl].push(item.n);
      if (item.n.left) queue.push({ n: item.n.left, level: lvl + 1, idx: 0 });
      if (item.n.right) queue.push({ n: item.n.right, level: lvl + 1, idx: 0 });
    }
    const contentWidth = width - padding * 2;
    const posMap: any = new Map();
    const maxLevel = Math.max(0, ...Object.keys(levels).map(v => parseInt(v, 10)));
    for (let l = 0; l <= maxLevel; l++) {
      const row = levels[l] || [];
      const count = row.length || 1;
      for (let i = 0; i < row.length; i++) {
        const x = padding + Math.round(((i + 1) * contentWidth) / (count + 1));
        const y = padding + l * levelGap + 20;
        posMap.set(row[i], { x, y });
        nodes.push({ key: row[i].key, x, y });
      }
    }
    const addLines = (n: any) => {
      const p = posMap.get(n);
      if (n.left) {
        const c = posMap.get(n.left);
        const vy = p.y + 20;
        const vh = c.y - p.y - 20;
        const vx = p.x;
        const hx = Math.min(p.x, c.x);
        const hy = c.y;
        const hw = Math.abs(c.x - p.x);
        lines.push({ vx, vy, vh, hx, hy, hw });
        addLines(n.left);
      }
      if (n.right) {
        const c = posMap.get(n.right);
        const vy = p.y + 20;
        const vh = c.y - p.y - 20;
        const vx = p.x;
        const hx = Math.min(p.x, c.x);
        const hy = c.y;
        const hw = Math.abs(c.x - p.x);
        lines.push({ vx, vy, vh, hx, hy, hw });
        addLines(n.right);
      }
    };
    addLines(r);
    return { nodes, lines };
  }
};

const BubbleSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; a: number; b: number; swapped: boolean; sorted: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (base: number[]) => {
    const a = base.slice();
    const n = a.length;
    const out: { arr: number[]; a: number; b: number; swapped: boolean; sorted: number }[] = [{ arr: a.slice(), a: -1, b: -1, swapped: false, sorted: n }];
    for (let i = n - 1; i > 0; i--) {
      for (let j = 0; j < i; j++) {
        const swapped = a[j] > a[j + 1];
        if (swapped) {
          const t = a[j];
          a[j] = a[j + 1];
          a[j + 1] = t;
        }
        out.push({ arr: a.slice(), a: j, b: j + 1, swapped, sorted: i });
      }
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { a: -1, b: -1, swapped: false, sorted: arr.length };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.bbCard}>
      <View style={styles.bbHeader}> 
        <Text style={styles.bbTitle}>冒泡排序 · 玻璃风格</Text>
        <Text style={styles.bbSubtitle}>播放、步进、重置、随机与速度</Text>
      </View>
      <View style={styles.bbControls}>
        <TouchableOpacity style={[styles.bbBtn, playing ? styles.bbBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_BUBBLE.pause : ICON_BASE64_BUBBLE.play }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bbBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUBBLE.step }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bbBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUBBLE.reset }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.bbBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_BUBBLE.shuffle }} style={styles.bbIcon} />
          <Text style={styles.bbBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.bbChips}>
          <TouchableOpacity style={styles.bbChip} onPress={() => setSpeed(900)}><Text style={styles.bbChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.bbChip, styles.bbChipActive]} onPress={() => setSpeed(600)}><Text style={styles.bbChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.bbChip} onPress={() => setSpeed(300)}><Text style={styles.bbChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.bbBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inCompare = i === cur.a || i === cur.b;
          const swapped = inCompare && cur.swapped;
          const sorted = i >= cur.sorted;
          return (
            <View key={i} style={[styles.bbBar, { height: h }, inCompare ? styles.bbBarHL : null, swapped ? styles.bbBarSwap : null, sorted ? styles.bbBarSorted : null]}>
              <Text style={styles.bbBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.bbFooterRow}>
        <Text style={styles.bbFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.bbFootText, isDone ? styles.bbDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};

const SelectionSortVisualizer: React.FC = () => {
  const [arr, setArr] = useState<number[]>([]);
  const [steps, setSteps] = useState<{ arr: number[]; i: number; j: number; minIdx: number; swapped: boolean; sortedStart: number }[]>([]);
  const [idx, setIdx] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [speed, setSpeed] = useState(600);
  const timerRef = React.useRef<any>(null);
  const maxBars = 18;

  const genArray = () => {
    const a = Array.from({ length: maxBars }, () => Math.floor(20 + Math.random() * 120));
    setArr(a);
    const s = buildSteps(a);
    setSteps(s);
    setIdx(0);
    setPlaying(false);
  };

  const buildSteps = (base: number[]) => {
    const a = base.slice();
    const n = a.length;
    const out: { arr: number[]; i: number; j: number; minIdx: number; swapped: boolean; sortedStart: number }[] = [{ arr: a.slice(), i: -1, j: -1, minIdx: -1, swapped: false, sortedStart: 0 }];
    for (let i = 0; i < n - 1; i++) {
      let minIdx = i;
      for (let j = i + 1; j < n; j++) {
        if (a[j] < a[minIdx]) minIdx = j;
        out.push({ arr: a.slice(), i, j, minIdx, swapped: false, sortedStart: i });
      }
      if (minIdx !== i) {
        const t = a[i]; a[i] = a[minIdx]; a[minIdx] = t;
        out.push({ arr: a.slice(), i, j: minIdx, minIdx, swapped: true, sortedStart: i });
      }
      out.push({ arr: a.slice(), i, j: -1, minIdx, swapped: false, sortedStart: i + 1 });
    }
    return out;
  };

  const stepOnce = () => {
    setIdx(prev => {
      const next = Math.min(prev + 1, steps.length - 1);
      setArr(steps[next].arr);
      if (next === steps.length - 1) setPlaying(false);
      return next;
    });
  };

  const togglePlay = () => setPlaying(p => !p);
  const resetSort = () => { const s = buildSteps(arr.slice()); setSteps(s); setIdx(0); setPlaying(false); };
  const shuffle = () => genArray();

  React.useEffect(() => { genArray(); }, []);
  React.useEffect(() => {
    if (playing) {
      if (timerRef.current) clearInterval(timerRef.current);
      timerRef.current = setInterval(stepOnce, speed);
    } else {
      if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; }
    }
    return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } };
  }, [playing, speed, steps]);

  const maxVal = Math.max(...arr, 1);
  const cur = steps[idx] || { i: -1, j: -1, minIdx: -1, swapped: false, sortedStart: 0 };
  const isDone = idx === steps.length - 1 && steps.length > 1;

  return (
    <View style={styles.ssCard}>
      <View style={styles.ssHeader}> 
        <Text style={styles.ssTitle}>选择排序 · 海洋风格</Text>
        <Text style={styles.ssSubtitle}>播放、步进、重置、随机与速度</Text>
      </View>
      <View style={styles.ssControls}>
        <TouchableOpacity style={[styles.ssBtn, playing ? styles.ssBtnActive : null]} onPress={togglePlay} activeOpacity={0.85}>
          <Image source={{ uri: playing ? ICON_BASE64_SELECT.pause : ICON_BASE64_SELECT.play }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>{playing ? '暂停' : '播放'}</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ssBtn} onPress={stepOnce} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SELECT.step }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>步进</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ssBtn} onPress={resetSort} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SELECT.reset }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>重置</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.ssBtn} onPress={shuffle} activeOpacity={0.85}>
          <Image source={{ uri: ICON_BASE64_SELECT.shuffle }} style={styles.ssIcon} />
          <Text style={styles.ssBtnText}>随机</Text>
        </TouchableOpacity>
        <View style={styles.ssChips}>
          <TouchableOpacity style={styles.ssChip} onPress={() => setSpeed(900)}><Text style={styles.ssChipText}>0.5×</Text></TouchableOpacity>
          <TouchableOpacity style={[styles.ssChip, styles.ssChipActive]} onPress={() => setSpeed(600)}><Text style={styles.ssChipText}>1×</Text></TouchableOpacity>
          <TouchableOpacity style={styles.ssChip} onPress={() => setSpeed(300)}><Text style={styles.ssChipText}>2×</Text></TouchableOpacity>
        </View>
      </View>
      <View style={styles.ssBars}>
        {arr.map((v, i) => {
          const h = Math.round((v / maxVal) * 160) + 20;
          const inCompare = i === cur.i || i === cur.j;
          const isMin = i === cur.minIdx;
          const swapped = inCompare && cur.swapped;
          const sorted = i < cur.sortedStart;
          return (
            <View key={i} style={[styles.ssBar, { height: h }, inCompare ? styles.ssBarHL : null, isMin ? styles.ssBarMin : null, swapped ? styles.ssBarSwap : null, sorted ? styles.ssBarSorted : null]}>
              <Text style={styles.ssBarText}>{v}</Text>
            </View>
          );
        })}
      </View>
      <View style={styles.ssFooterRow}>
        <Text style={styles.ssFootText}>步骤 {idx} / {Math.max(steps.length - 1, 0)}</Text>
        <Text style={[styles.ssFootText, isDone ? styles.ssDone : null]}>{isDone ? '已完成排序' : '进行中'}</Text>
      </View>
    </View>
  );
};
export default CalendarComponentApp;

这段React Native日历组件代码在架构设计上与鸿蒙系统的核心理念有着深刻的共鸣。从原理层面分析,该代码采用了典型的声明式UI编程范式,通过状态驱动界面更新,这与鸿蒙ArkUI的响应式设计思想高度契合。

代码通过Icon组件展示了资源抽象化的设计理念,将具体的图标实现与使用逻辑分离,通过统一的接口来管理不同的图标资源。这种设计模式正是鸿蒙原子化服务思想的体现,每个图标都被封装成独立的服务单元,可以在不同的上下文中被复用。这种组件化的思维方式与鸿蒙的分布式架构有着内在的一致性,都强调功能的模块化和服务的可组合性。

在日历主体的实现中,代码通过useState管理当前显示月份的状态,所有界面元素都基于这一状态进行渲染。这种单向数据流的架构确保了界面与数据的一致性,这正是鸿蒙声明式UI的核心优势所在。组件内部的状态变化会自动触发界面的重新渲染,实现了数据与UI的自动同步,这种机制与鸿蒙的状态管理在本质上相通。

日期计算模块展现了算法抽象化的设计思想,将复杂的日历逻辑封装在独立的函数中,通过清晰的接口对外提供服务。这种功能封装模式与鸿蒙Ability组件的能力划分有着相似的设计哲学,都致力于构建清晰、可维护的软件架构。

事件处理机制通过回调函数实现了组件间的松耦合通信,这种设计模式与鸿蒙的分布式通信机制在理念层面高度一致。通过onDateSelect等回调接口,日历组件可以与外部系统进行数据交换,这种接口设计思想正是鸿蒙跨设备协同能力的基础。

界面渲染采用分层架构,将日历拆分为头部导航、星期栏和日期网格三个独立的渲染模块。这种模块化设计使得每个部分都可以独立开发和测试,这与鸿蒙的弹性部署能力相呼应。每个渲染函数都专注于特定的界面区域,这种关注点分离的设计原则正是构建大型复杂应用的关键所在。

交互设计方面,代码通过TouchableOpacity组件提供了原生的触控反馈,这种对用户体验的重视与鸿蒙的人机交互设计理念相吻合。导航按钮的点击事件处理展现了状态变更的完整生命周期,从用户操作到状态更新再到界面重绘,这一流程与鸿蒙的UI更新机制在原理层面高度一致。


打包

接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

相关推荐
2401_860319523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:SwipeCell 滑动单元格(可以左右滑动来展示操作按钮的单元格组件)
javascript·react native·ecmascript
2401_860319523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Circle 环形进度条(圆环形的进度条组件)
react native·react.js·harmonyos
爱吃大芒果3 小时前
Flutter 开发环境配置避坑指南:Windows/macOS/Linux 全平台
flutter·华为·harmonyos
花先锋队长3 小时前
华为Mate X7测评:折叠屏的尽头,是让你忘记它在折叠?
科技·华为·智能手机·harmonyos
赵财猫._.3 小时前
React Native鸿蒙开发实战(一):环境搭建与第一个应用
react native·react.js·华为·harmonyos
2401_860494703 小时前
在React Native鸿蒙跨平台开发中实现一个计数排序算法,如何使用一个额外的数组来统计每个值的出现次数,然后根据这个统计结果来重构原数组的顺序
javascript·react native·react.js·重构·ecmascript·排序算法
赵财猫._.4 小时前
【Flutter x 鸿蒙】第六篇:状态管理、数据持久化与分布式数据
分布式·flutter·harmonyos
2401_860494704 小时前
在React Native鸿蒙跨平台开发中实现一个基数排序算法,如何进行找到最大数:遍历数组找到最大值呢?
javascript·算法·react native·react.js·排序算法·harmonyos
ElenaYu4 小时前
在 Mac 上用 scrcpy 投屏 Honor 300 Pro(鸿蒙/Android)并支持鼠标点击控制
android·macos·harmonyos