ubuntu安装应用脚本


推荐将某个文件夹专门放置应用,然后在里面,放置这两个sh文件:
在该目录下打开终端,使用
sudo ./install.sh 安装,选择序号,然后一直回车
sudo ./uninstall.sh 卸载,选择序号卸载

对需要沙箱的应用,在/usr/share/applications快捷方式的应用路径后面添加 --no-sandbox,做了这步要注销登陆重新进入才生效

安装
install.sh:

sh
#!/usr/bin/env bash
set -euo pipefail

# === 工具函数 ===
clean_name() {
  # 去掉 .AppImage 后缀,替换空格/点为下划线,连字符压缩为1个,转小写  # 中文注释
  echo "$1" | sed -E 's/\.(AppImage|appimage)$//i' | tr '. ' '_' | tr -s '_' | tr '[:upper:]' '[:lower:]'
}

say() { echo -e "$*"; }  # 统一中文输出  # 中文注释

# === 必须 root 运行 ===
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
  say "请使用 sudo 运行本脚本"
  exit 1
fi

# === 获取本脚本名称,避免把自己加进列表 ===
SCRIPT_NAME="$(basename "$0")"

# === 扫描当前目录的所有 AppImage 和 可执行文件 并让用户选择 ===
# 使用 -executable 查找可执行文件,排除目录和本脚本自身
mapfile -d '' FILES < <(find . -maxdepth 1 -type f \( -iname '*.AppImage' -o -executable \) ! -name "$SCRIPT_NAME" -print0 | sort -z)

if [ "${#FILES[@]}" -eq 0 ]; then
  say "错误:当前目录未找到任何 AppImage 或可执行二进制文件"
  exit 1
fi

say "检测到以下可执行程序:"
i=1
for f in "${FILES[@]}"; do
  fname="${f#./}"
  fsize="$(stat -c '%s' "$fname" 2>/dev/null || echo 0)"
  # 人性化大小
  if [ "$fsize" -ge 1073741824 ]; then sz="$(awk "BEGIN{printf \"%.2fG\", $fsize/1073741824}")"
  elif [ "$fsize" -ge 1048576 ]; then sz="$(awk "BEGIN{printf \"%.2fM\", $fsize/1048576}")"
  elif [ "$fsize" -ge 1024 ]; then sz="$(awk "BEGIN{printf \"%.2fK\", $fsize/1024}")"
  else sz="${fsize}B"; fi
  
  # 标记文件类型
  if [[ "$fname" =~ \.AppImage$ ]] || [[ "$fname" =~ \.appimage$ ]]; then
      type_tag="[AppImage]"
  else
      type_tag="[二进制]"
  fi
  
  say "  $i) $type_tag ${fname} (${sz})"
  i=$((i+1))
done

read -p "请输入要安装的序号 [1-${#FILES[@]}]: " PICK
if ! [[ "$PICK" =~ ^[0-9]+$ ]] || [ "$PICK" -lt 1 ] || [ "$PICK" -gt "${#FILES[@]}" ]; then
  say "错误:输入的序号无效"
  exit 1
fi

FILE_REL="${FILES[$((PICK-1))]}"
FILE_PATH="$(readlink -f "${FILE_REL#./}")"           # 绝对路径  # 中文注释
FILE_BASE="$(basename "$FILE_PATH")"                  # 文件名(含扩展)  # 中文注释
CLEAN_NAME="$(clean_name "$FILE_BASE")"               # 清洗名  # 中文注释
WORKDIR="$(pwd -P)"                                   # 当前目录  # 中文注释
chmod +x "$FILE_PATH"                                 # 确保可执行  # 中文注释

# 判断是否为 AppImage
IS_APPIMAGE=false
if [[ "$FILE_BASE" =~ \.AppImage$ ]] || [[ "$FILE_BASE" =~ \.appimage$ ]]; then
    IS_APPIMAGE=true
fi

# === 先创建 /opt 目标目录(图标与应用都存这里) ===
TARGET_DIR="/opt/$CLEAN_NAME"
mkdir -p "$TARGET_DIR"                                 # 创建应用目录  # 中文注释

# === 提取或生成图标 ===
ICON_LOCAL_PATH=""                                     # /opt/<clean_name>/ 内的图标绝对路径  # 中文注释

if [ "$IS_APPIMAGE" = true ]; then
    say "正在挂载 AppImage(不解包)以提取图标……"
    MNT=""                                             # 挂载点路径  # 中文注释
    FUSE_PID=""                                        # FUSE 进程 PID(用于回收)  # 中文注释

    cleanup_mount() {                                  # 收尾函数  # 中文注释
      if [ -n "${MNT:-}" ] && mountpoint -q "$MNT"; then
        umount "$MNT" || true
      fi
      [ -n "${MNT:-}" ] && rmdir "$MNT" 2>/dev/null || true
      if [ -n "${FUSE_PID:-}" ]; then
        kill -INT "$FUSE_PID" 2>/dev/null || true
        wait "$FUSE_PID" 2>/dev/null || true
      fi
    }
    trap cleanup_mount EXIT

    # 1) 尝试内核只读挂载(loop+offset)
    USE_KERNEL_MOUNT=false
    if OFF="$("$FILE_PATH" --appimage-offset 2>/dev/null)"; then
      if [[ "$OFF" =~ ^[0-9]+$ ]]; then
        MNT="$(mktemp -d)"
        if mount -o "ro,loop,offset=$OFF" -t squashfs "$FILE_PATH" "$MNT" 2>/dev/null; then
          USE_KERNEL_MOUNT=true
          say "已使用内核只读挂载方式。"
        fi
      fi
    fi

    # 2) 若失败,回退到 FUSE 挂载(--appimage-mount)
    if [ "$USE_KERNEL_MOUNT" = false ]; then
      TMP_OUT="$(mktemp)"
      "$FILE_PATH" --appimage-mount >"$TMP_OUT" & FUSE_PID=$!
      for _ in $(seq 1 50); do
        if [ -s "$TMP_OUT" ]; then break; fi
        sleep 0.1
      done
      if [ ! -s "$TMP_OUT" ]; then
        say "错误:无法挂载 AppImage,提取图标失败。"
        exit 1
      fi
      MNT="$(head -n1 "$TMP_OUT")"
      rm -f "$TMP_OUT"
      say "已使用 FUSE 临时挂载方式。"
    fi

    # 3) 搜索规则:先根目录(不递归)找图片 → 若无,再到 usr/share/ 下找第一张
    find_first_image() {
      local root="$1"
      find "$root" -maxdepth 1 -type f \( -iname '*.svg' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.ico' \) | head -n1
    }
    find_first_image_in_usr_share() {
      local root="$1"
      local base="$root/usr/share"
      [ -d "$base" ] || return 0
      find "$base" -type f \( -iname '*.svg' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.ico' \) | head -n1
    }

    CAND="$(find_first_image "$MNT")"
    if [ -z "${CAND:-}" ]; then
      CAND="$(find_first_image_in_usr_share "$MNT")"
    fi

    if [ -n "${CAND:-}" ]; then
      EXT="${CAND##*.}"
      ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.${EXT}"
      cp -f "$CAND" "$ICON_LOCAL_PATH"
      chmod 644 "$ICON_LOCAL_PATH" || true
      say "已提取图标到:$ICON_LOCAL_PATH"
    fi
else
    say "处理普通二进制文件,尝试寻找同级目录下的同名图标……"
    # 尝试在当前目录找同名的 .png 或 .svg
    BASENAME_NOEXT="${FILE_BASE%.*}"
    if [ -f "$WORKDIR/$BASENAME_NOEXT.png" ]; then
        ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.png"
        cp -f "$WORKDIR/$BASENAME_NOEXT.png" "$ICON_LOCAL_PATH"
        chmod 644 "$ICON_LOCAL_PATH" || true
        say "找到同名图片,已复制为图标:$ICON_LOCAL_PATH"
    elif [ -f "$WORKDIR/$BASENAME_NOEXT.svg" ]; then
        ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.svg"
        cp -f "$WORKDIR/$BASENAME_NOEXT.svg" "$ICON_LOCAL_PATH"
        chmod 644 "$ICON_LOCAL_PATH" || true
        say "找到同名矢量图,已复制为图标:$ICON_LOCAL_PATH"
    fi
fi

# 兜底:如果 AppImage 没提取出图片,或者二进制没找到同名图片,则生成默认 SVG
if [ -z "$ICON_LOCAL_PATH" ]; then
  ICON_LOCAL_PATH="$TARGET_DIR/${CLEAN_NAME}.svg"
  cat > "$ICON_LOCAL_PATH" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <rect x="5" y="5" width="90" height="90" rx="20" ry="20" fill="#4A90E2"/>
  <circle cx="50" cy="50" r="30" fill="#FFFFFF" opacity="0.2"/>
  <path d="M35 35 L65 35 L65 65 L35 65 Z" fill="none" stroke="#FFFFFF" stroke-width="6"/>
  <path d="M42 42 L58 42 L58 58 L42 58 Z" fill="#FFFFFF"/>
</svg>
EOF
  chmod 644 "$ICON_LOCAL_PATH" || true
  say "未找到匹配的图标,已生成默认图标:$ICON_LOCAL_PATH"
fi

# === 交互信息 ===
BASENAME="$(basename "$FILE_BASE" .AppImage)"
# 二进制文件如果带后缀,继续去掉
BASENAME="${BASENAME%.*}"

read -p "请输入应用显示名称(回车默认:$BASENAME): " APP_NAME
APP_NAME=${APP_NAME:-$BASENAME}
read -p "请输入应用描述(回车默认:Application $APP_NAME): " DESCRIPTION
DESCRIPTION=${DESCRIPTION:-"Application $APP_NAME"}

say "请选择应用分类:"
say "1) 工具 Utility"
say "2) 图形 Graphics"
say "3) 开发 Development"
say "4) 办公 Office"
say "5) 网络 Network"
read -p "请输入序号(回车默认:1): " CATEGORY_NUM
case ${CATEGORY_NUM:-1} in
  2) CATEGORY="Graphics;2DGraphics;3DGraphics;";;
  3) CATEGORY="Development;IDE;";;
  4) CATEGORY="Office;";;
  5) CATEGORY="Network;";;
  *) CATEGORY="Utility;";;
esac

# === 可选:检查并安装 libfuse2(仅针对 AppImage) ===
if [ "$IS_APPIMAGE" = true ]; then
    say "检查 libfuse2……"
    if ! dpkg -s libfuse2 >/dev/null 2>&1; then
      say "未检测到 libfuse2,正在安装(不执行 apt-get update)……"
      if ! apt-get install -y libfuse2; then
        say "安装 libfuse2 失败(可能需要你稍后手动执行 apt-get update 再安装)。"
      fi
    else
      say "已安装 libfuse2。"
    fi
fi

# === 将程序拷贝到 /opt/<clean_name>/ 并赋权 ===
say "正在安装到:$TARGET_DIR"
cp -f "$FILE_PATH" "$TARGET_DIR/"
chmod +x "$TARGET_DIR/$FILE_BASE"

# === 创建 .desktop:Exec 和 Icon 都指向 /opt/<clean_name>/ ===
DESKTOP_FILE="/usr/share/applications/${CLEAN_NAME}.desktop"
ICON_ABS="$ICON_LOCAL_PATH"

say "正在创建桌面启动器:$DESKTOP_FILE"
cat > "$DESKTOP_FILE" <<EOF
[Desktop Entry]
Version=1.0
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=$TARGET_DIR/$FILE_BASE
Icon=$ICON_ABS
Terminal=false
Type=Application
Categories=$CATEGORY
StartupNotify=true
EOF

# === 刷新 desktop 数据库 ===
if command -v update-desktop-database >/dev/null 2>&1; then
  update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
fi

say "安装完成!"
say "—— 主程序:$TARGET_DIR/$FILE_BASE"
say "—— 图标  :$ICON_ABS"
say "—— 启动器:$DESKTOP_FILE"
say "如图标未立即出现,可尝试注销/重启图形会话。"

卸载
uninstall.sh:

sh
#!/bin/bash

# =========================
# AppImage 卸载脚本(中文版)
# 从 /usr/share/applications 中筛选 Exec 行含 ".AppImage" 的桌面项
# 并按所选项删除 /opt/<app_id>、对应 .desktop、以及 hicolor 主题下的图标文件
# =========================

# 必须用 sudo/root 运行
if [ "$EUID" -ne 0 ]; then 
    echo "请使用 sudo 运行本脚本"
    exit 1
fi

echo "已安装的 AppImage 应用:"
echo "------------------------"

counter=1
declare -a desktop_files

# 列出所有包含 .AppImage 的 .desktop(更稳健:匹配 Exec 行)
while IFS= read -r file; do
    if grep -qE '^Exec=.*\.AppImage' "$file"; then
        # 优先展示 Name=,没有就用文件名
        name=$(grep -E '^Name=' "$file" | head -n1 | cut -d= -f2-)
        [ -z "$name" ] && name=$(basename "$file" .desktop)
        echo "$counter) $name  [$(basename "$file")]"
        desktop_files+=("$file")
        ((counter++))
    fi
done < <(find /usr/share/applications -type f -name "*.desktop" 2>/dev/null | sort)

if [ ${#desktop_files[@]} -eq 0 ]; then
    echo "未发现通过 AppImage 方式安装的应用。"
    exit 0
fi

# 选择要卸载的应用
read -p "请输入要卸载的序号 (1-$((counter-1))): " selection

# 校验输入
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt $((counter-1)) ]; then
    echo "无效的选择"
    exit 1
fi

# 解析选择项
selected_file="${desktop_files[$((selection-1))]}"
app_id=$(basename "$selected_file" .desktop)  # 安装脚本中用的 CLEAN_NAME 即为此 app_id
app_name=$(grep -E '^Name=' "$selected_file" | head -n1 | cut -d= -f2-)
[ -z "$app_name" ] && app_name="$app_id"

echo "正在卸载:$app_name ($app_id)"

# 【关键点-删除程序主体】移除 /opt/<app_id> 目录(AppImage 主体位于此处)
if [ -d "/opt/$app_id" ]; then
    echo "移除程序目录:/opt/$app_id"
    rm -rf "/opt/$app_id"
fi

# 【关键点-删除桌面启动器】移除 .desktop 文件
echo "移除桌面启动器:$selected_file"
rm "$selected_file"

# Remove icon
if [ -f "/usr/share/icons/hicolor/scalable/apps/$app_name.svg" ]; then
    echo "Removing icon..."
    rm "/usr/share/icons/hicolor/scalable/apps/$app_name.svg"
fi

# Update system caches
echo "更新系统缓存"
update-desktop-database /usr/share/applications
gtk-update-icon-cache -f /usr/share/icons/hicolor

echo "卸载完成!您可能需要注销并重新登录才能在启动器中看到更改。"

带文件夹应用

sh
#!/bin/bash
# ============================================================
# 通用便携式应用安装脚本 (交互选择版)
# 功能:扫描当前目录子文件夹 → 用户选择 → 安装到 /opt/
#       关键文件(可执行/图标/desktop)均列出供用户确认
# ============================================================

set -e

# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'

# 脚本所在目录
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"

# ============================================================
# 通用选择函数
# 参数: $1=提示文字  $2=默认序号(留空表示无默认)  $3..=候选项
# 返回: 用户选中的项 (通过 echo)
# ============================================================
select_from_list() {
    local prompt="$1"
    local default_idx="$2"
    shift 2
    local items=("$@")

    if [ ${#items[@]} -eq 0 ]; then
        echo ""
        return 1
    fi

    echo -e "${CYAN}$prompt${NC}" >&2
    for i in "${!items[@]}"; do
        local marker="  "
        if [ -n "$default_idx" ] && [ "$((i+1))" = "$default_idx" ]; then
            marker="${GREEN}★${NC} "
        fi
        printf "  ${marker}${YELLOW}%2d)${NC} %s\n" "$((i+1))" "${items[$i]}" >&2
    done

    local hint="(1-${#items[@]})"
    [ -n "$default_idx" ] && hint="$hint,回车使用默认 ★$default_idx"

    local choice
    while true; do
        read -p "请输入序号 $hint: " choice </dev/tty
        # 空输入 → 用默认
        if [ -z "$choice" ] && [ -n "$default_idx" ]; then
            choice="$default_idx"
        fi
        # 校验
        if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#items[@]} ]; then
            echo "${items[$((choice-1))]}"
            return 0
        fi
        echo -e "${RED}无效输入,请重新输入${NC}" >&2
    done
}

# ============================================================
# 主流程
# ============================================================
echo -e "${BLUE}======================================${NC}"
echo -e "${BLUE}  便携式应用安装脚本${NC}"
echo -e "${BLUE}======================================${NC}"
echo -e "扫描目录:${YELLOW}$SCRIPT_DIR${NC}"
echo ""

# ---------- 1. 选择应用文件夹 ----------
mapfile -t DIRS < <(find . -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | sort)
if [ ${#DIRS[@]} -eq 0 ]; then
    echo -e "${RED}错误:当前目录下没有任何子文件夹!${NC}"
    exit 1
fi

SELECTED_DIR=$(select_from_list "请选择要安装的应用文件夹:" "" "${DIRS[@]}")
SOURCE_PATH="$SCRIPT_DIR/$SELECTED_DIR"
echo -e "${GREEN}✓ 已选择:$SELECTED_DIR${NC}"
echo ""

# ---------- 2. 输入应用名称 ----------
DEFAULT_NAME=$(echo "$SELECTED_DIR" | cut -d'-' -f1 | tr '[:upper:]' '[:lower:]')
read -p "$(echo -e ${CYAN}请输入安装后的应用名称${NC} [默认: ${YELLOW}$DEFAULT_NAME${NC}]: )" APP_NAME </dev/tty
APP_NAME=${APP_NAME:-$DEFAULT_NAME}
TARGET_PATH="/opt/$APP_NAME"
echo -e "${GREEN}✓ 将安装到:$TARGET_PATH${NC}"
echo ""

# ---------- 3. 选择主可执行文件 ----------
# 列出所有可执行的二进制(排除 .sh/.svg/.png/.txt/.so 等)
mapfile -t EXEC_CANDIDATES < <(
    find "$SOURCE_PATH" -maxdepth 2 -type f -executable \
        ! -name "*.svg" ! -name "*.png" ! -name "*.txt" \
        ! -name "*.so*" ! -name "*.desktop" \
        -printf '%P\n' | sort
)

if [ ${#EXEC_CANDIDATES[@]} -eq 0 ]; then
    echo -e "${RED}错误:未找到任何可执行文件${NC}"
    exit 1
fi

# 找默认序号:精确同名优先
DEFAULT_EXEC_IDX=""
for i in "${!EXEC_CANDIDATES[@]}"; do
    if [ "${EXEC_CANDIDATES[$i]}" = "$APP_NAME" ]; then
        DEFAULT_EXEC_IDX="$((i+1))"
        break
    fi
done
# 没精确匹配 → 找包含应用名的
if [ -z "$DEFAULT_EXEC_IDX" ]; then
    for i in "${!EXEC_CANDIDATES[@]}"; do
        if [[ "${EXEC_CANDIDATES[$i]}" == *"$APP_NAME"* ]] && [[ "${EXEC_CANDIDATES[$i]}" != *.sh ]]; then
            DEFAULT_EXEC_IDX="$((i+1))"
            break
        fi
    done
fi
# 都没有 → 第一个
[ -z "$DEFAULT_EXEC_IDX" ] && DEFAULT_EXEC_IDX="1"

EXEC_REL=$(select_from_list "请选择主可执行文件:" "$DEFAULT_EXEC_IDX" "${EXEC_CANDIDATES[@]}")
EXEC_FILE="$TARGET_PATH/$EXEC_REL"
echo -e "${GREEN}✓ 可执行文件:$EXEC_REL${NC}"
echo ""

# ---------- 4. 选择图标文件 ----------
mapfile -t ICON_CANDIDATES < <(
    find "$SOURCE_PATH" -maxdepth 3 -type f \
        \( -name "*.svg" -o -name "*.png" -o -name "*.xpm" -o -name "*.ico" \) \
        -printf '%P\n' | sort
)

ICON_FILE=""
if [ ${#ICON_CANDIDATES[@]} -eq 0 ]; then
    echo -e "${YELLOW}⚠ 未找到图标文件,将使用默认图标${NC}"
else
    # 默认序号:优先 .svg 且包含应用名(且非 symbolic)
    DEFAULT_ICON_IDX=""
    for i in "${!ICON_CANDIDATES[@]}"; do
        item="${ICON_CANDIDATES[$i]}"
        if [[ "$item" == *"$APP_NAME"*.svg ]] && [[ "$item" != *symbolic* ]]; then
            DEFAULT_ICON_IDX="$((i+1))"
            break
        fi
    done
    # 退而求其次:含应用名的任何图标
    if [ -z "$DEFAULT_ICON_IDX" ]; then
        for i in "${!ICON_CANDIDATES[@]}"; do
            if [[ "${ICON_CANDIDATES[$i]}" == *"$APP_NAME"* ]]; then
                DEFAULT_ICON_IDX="$((i+1))"
                break
            fi
        done
    fi
    [ -z "$DEFAULT_ICON_IDX" ] && DEFAULT_ICON_IDX="1"

    # 添加"跳过"选项
    ICON_CANDIDATES+=("[跳过 - 不设置图标]")
    ICON_REL=$(select_from_list "请选择应用图标:" "$DEFAULT_ICON_IDX" "${ICON_CANDIDATES[@]}")
    if [[ "$ICON_REL" != "[跳过"* ]]; then
        ICON_FILE="$TARGET_PATH/$ICON_REL"
        echo -e "${GREEN}✓ 图标文件:$ICON_REL${NC}"
    else
        echo -e "${YELLOW}✓ 跳过图标设置${NC}"
    fi
fi
echo ""

# ---------- 5. 选择 .desktop 文件 ----------
mapfile -t DESKTOP_CANDIDATES < <(
    find "$SOURCE_PATH" -maxdepth 3 -type f -name "*.desktop" -printf '%P\n' | sort
)

DESKTOP_SRC=""
if [ ${#DESKTOP_CANDIDATES[@]} -eq 0 ]; then
    echo -e "${YELLOW}⚠ 未找到 .desktop 文件,将自动生成${NC}"
elif [ ${#DESKTOP_CANDIDATES[@]} -eq 1 ]; then
    DESKTOP_SRC="$SOURCE_PATH/${DESKTOP_CANDIDATES[0]}"
    echo -e "${GREEN}✓ Desktop 文件(自动选中唯一一个):${DESKTOP_CANDIDATES[0]}${NC}"
else
    DESKTOP_CANDIDATES+=("[跳过 - 自动生成新的]")
    DESKTOP_REL=$(select_from_list "请选择 .desktop 模板文件:" "1" "${DESKTOP_CANDIDATES[@]}")
    if [[ "$DESKTOP_REL" != "[跳过"* ]]; then
        DESKTOP_SRC="$SOURCE_PATH/$DESKTOP_REL"
        echo -e "${GREEN}✓ Desktop 文件:$DESKTOP_REL${NC}"
    fi
fi
echo ""

# ---------- 6. 安装确认 ----------
echo -e "${BLUE}======== 安装预览 ========${NC}"
echo -e "源目录:    ${YELLOW}$SOURCE_PATH${NC}"
echo -e "目标位置:  ${YELLOW}$TARGET_PATH${NC}"
echo -e "可执行文件:${YELLOW}$EXEC_FILE${NC}"
echo -e "图标:      ${YELLOW}${ICON_FILE:-(无)}${NC}"
echo -e "Desktop:   ${YELLOW}${DESKTOP_SRC:-(自动生成)}${NC}"
echo -e "命令链接:  ${YELLOW}/usr/local/bin/$APP_NAME${NC}"
echo -e "${BLUE}=========================${NC}"
read -p "确认安装?(Y/n): " CONFIRM </dev/tty
if [[ "$CONFIRM" =~ ^[Nn]$ ]]; then
    echo -e "${RED}已取消${NC}"
    exit 0
fi

# ---------- 7. 检查 sudo ----------
if [ "$EUID" -ne 0 ]; then
    echo -e "${YELLOW}需要管理员权限:${NC}"
    sudo -v || { echo -e "${RED}权限获取失败${NC}"; exit 1; }
fi

# ---------- 8. 处理已存在的目标 ----------
if [ -d "$TARGET_PATH" ]; then
    echo -e "${YELLOW}⚠ $TARGET_PATH 已存在${NC}"
    read -p "是否覆盖?(y/N): " OVERWRITE </dev/tty
    if [[ "$OVERWRITE" =~ ^[Yy]$ ]]; then
        sudo rm -rf "$TARGET_PATH"
    else
        echo -e "${RED}已取消${NC}"
        exit 1
    fi
fi

# ---------- 9. 移动并设权限 ----------
echo -e "${BLUE}[1/4] 移动到 $TARGET_PATH ...${NC}"
sudo mv "$SOURCE_PATH" "$TARGET_PATH"
sudo chmod -R 755 "$TARGET_PATH"

# 【关键修复】文件已移动到 /opt/,所以 DESKTOP_SRC 路径要更新
# 之前 DESKTOP_SRC 指向 $SOURCE_PATH(下载目录),mv 后那里已经没文件了
if [ -n "$DESKTOP_SRC" ]; then
    DESKTOP_REL_PATH="${DESKTOP_SRC#$SOURCE_PATH/}"
    DESKTOP_SRC="$TARGET_PATH/$DESKTOP_REL_PATH"
fi

# ---------- 10. 创建命令行软链接 ----------
echo -e "${BLUE}[2/4] 创建命令行链接 /usr/local/bin/$APP_NAME ...${NC}"
sudo ln -sf "$EXEC_FILE" "/usr/local/bin/$APP_NAME"

# ---------- 11. 处理 .desktop 文件 ----------
echo -e "${BLUE}[3/4] 注册到应用菜单 ...${NC}"
DESKTOP_TARGET="/usr/share/applications/$APP_NAME.desktop"

if [ -n "$DESKTOP_SRC" ] && [ -f "$DESKTOP_SRC" ]; then
    echo -e "  从模板复制:$DESKTOP_SRC"
    sudo cp "$DESKTOP_SRC" "$DESKTOP_TARGET"

    # 替换或追加 Exec=
    if sudo grep -q "^Exec=" "$DESKTOP_TARGET"; then
        sudo sed -i "s|^Exec=.*|Exec=$EXEC_FILE %f|" "$DESKTOP_TARGET"
    else
        echo "Exec=$EXEC_FILE %f" | sudo tee -a "$DESKTOP_TARGET" > /dev/null
    fi

    # 替换或追加 Icon=
    if [ -n "$ICON_FILE" ]; then
        if sudo grep -q "^Icon=" "$DESKTOP_TARGET"; then
            sudo sed -i "s|^Icon=.*|Icon=$ICON_FILE|" "$DESKTOP_TARGET"
        else
            echo "Icon=$ICON_FILE" | sudo tee -a "$DESKTOP_TARGET" > /dev/null
        fi
    fi
else
    # 自动生成
    echo -e "  ${YELLOW}未指定 desktop 模板,自动生成${NC}"
    sudo tee "$DESKTOP_TARGET" > /dev/null <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=${APP_NAME^}
GenericName=$APP_NAME
Comment=$APP_NAME application
Exec=$EXEC_FILE %f
Icon=${ICON_FILE:-application-x-executable}
Terminal=false
Categories=Utility;
StartupNotify=true
EOF
fi
sudo chmod 644 "$DESKTOP_TARGET"

# 验证 .desktop 文件
if command -v desktop-file-validate >/dev/null 2>&1; then
    if ! desktop-file-validate "$DESKTOP_TARGET" 2>/dev/null; then
        echo -e "  ${YELLOW}⚠ desktop 文件验证有警告(通常不影响使用)${NC}"
    fi
fi

# 确认文件确实存在
if [ -f "$DESKTOP_TARGET" ]; then
    echo -e "  ${GREEN}✓ 已创建:$DESKTOP_TARGET${NC}"
else
    echo -e "  ${RED}✗ 失败:$DESKTOP_TARGET 未生成${NC}"
fi

# ---------- 12. 刷新菜单缓存 ----------
echo -e "${BLUE}[4/4] 刷新应用菜单缓存 ...${NC}"
sudo update-desktop-database /usr/share/applications/ 2>/dev/null || true

# ---------- 完成 ----------
echo ""
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}  ✓ 安装完成!${NC}"
echo -e "${GREEN}======================================${NC}"
echo -e "应用位置:${YELLOW}$TARGET_PATH${NC}"
echo -e "终端命令:${YELLOW}$APP_NAME${NC}"
echo -e "应用菜单:在 Mint 菜单中搜索 ${YELLOW}${APP_NAME^}${NC}"
echo ""
echo -e "${BLUE}卸载方法:${NC}"
echo -e "  sudo rm -rf $TARGET_PATH"
echo -e "  sudo rm /usr/local/bin/$APP_NAME"
echo -e "  sudo rm $DESKTOP_TARGET"