Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/locales/languages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@
"device.control.install.success": "Successfully installed {totalCount} apps to {deviceName}, {successCount} succeeded, {failCount} failed",
"device.control.install.success.single": "Successfully installed app to {deviceName}",
"device.control.install.error": "Install failed, please check app and try again",
"device.control.install.dragDrop.title": "Drop APK files here",
"device.control.install.dragDrop.subtitle": "Release to install applications",
"device.control.install.dragDrop.noApkFiles": "No APK files found in the dropped files",
"device.control.install.dragDrop.noDevices": "No connected devices available for installation",
"device.control.install.dragDrop.selectDevice": "Select Device",
"device.control.install.dragDrop.selectDeviceMessage": "Multiple devices detected. Please select a device to install the APK files:",
"device.control.install.dragDrop.error": "Failed to install APK files via drag and drop",
"device.control.file.name": "File Manager",
"device.control.file.push": "Push File",
"device.control.file.push.placeholder": "Please select the file to push",
Expand Down
7 changes: 7 additions & 0 deletions src/locales/languages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@
"device.control.install.success": "已成功将应用安装到 {deviceName} 中,共 {totalCount}个,成功 {successCount}个,失败 {failCount}个",
"device.control.install.success.single": "已成功将应用安装到 {deviceName} 中",
"device.control.install.error": "安装应用失败,请检查安装包后重试",
"device.control.install.dragDrop.title": "将 APK 文件拖拽到此处",
"device.control.install.dragDrop.subtitle": "松开鼠标以安装应用",
"device.control.install.dragDrop.noApkFiles": "拖拽的文件中未找到 APK 文件",
"device.control.install.dragDrop.noDevices": "没有连接的设备可用于安装",
"device.control.install.dragDrop.selectDevice": "选择设备",
"device.control.install.dragDrop.selectDeviceMessage": "检测到多个设备,请选择要安装 APK 文件的设备:",
"device.control.install.dragDrop.error": "拖拽安装 APK 文件失败",
"device.control.file.name": "文件管理",
"device.control.file.push": "推送文件",
"device.control.file.push.placeholder": "请选择要推送的文件",
Expand Down
190 changes: 189 additions & 1 deletion src/pages/device/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
<template>
<div class="h-full flex flex-col">
<div
class="h-full flex flex-col"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleDrop"
:class="{ 'drag-active': isDragActive }"
>
<!-- Drag and Drop Overlay -->
<div
v-if="isDragActive"
class="absolute inset-0 z-50 bg-primary-500/20 border-2 border-dashed border-primary-500 flex items-center justify-center pointer-events-none"
>
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-lg text-center">
<el-icon class="text-4xl text-primary-500 mb-2">
<Upload />
</el-icon>
<div class="text-lg font-medium text-gray-900 dark:text-white">
{{ $t('device.control.install.dragDrop.title') }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">
{{ $t('device.control.install.dragDrop.subtitle') }}
</div>
</div>
</div>
<BatchActions
class="overflow-hidden transition-all"
:class="isMultipleRow ? 'max-h-12 opacity-100 mb-2' : 'max-h-0 opacity-0 mb-0'"
Expand Down Expand Up @@ -181,6 +204,7 @@ const loading = ref(false)
const deviceList = ref([])
const mirrorActionRefs = ref([])
const selectionRows = ref([])
const isDragActive = ref(false)

const { proxy } = getCurrentInstance()

Expand All @@ -205,6 +229,170 @@ const remarkFilters = computed(() => {
return uniqBy(value, 'value')
})

// Drag and Drop functionality
function handleDragOver(event) {
// Check if any files being dragged are APK files
const files = event.dataTransfer.files
const types = event.dataTransfer.types

if (types.includes('Files') || files.length > 0) {
isDragActive.value = true
}
}

function handleDragLeave(event) {
// Only hide overlay if we're leaving the main container
if (!event.currentTarget.contains(event.relatedTarget)) {
isDragActive.value = false
}
}

async function handleDrop(event) {
isDragActive.value = false

const files = Array.from(event.dataTransfer.files)
const apkFiles = files.filter(file => file.name.toLowerCase().endsWith('.apk'))

if (apkFiles.length === 0) {
proxy.$message.warning(proxy.$t('device.control.install.dragDrop.noApkFiles'))
return
}

// Get available devices for installation
const availableDevices = deviceList.value.filter(device => device.status === 'device')

if (availableDevices.length === 0) {
proxy.$message.warning(proxy.$t('device.control.install.dragDrop.noDevices'))
return
}

// If only one device, install directly
if (availableDevices.length === 1) {
await installApkFiles(availableDevices[0], apkFiles)
return
}

// Multiple devices - show device selection dialog
try {
const selectedDevice = await showDeviceSelectionDialog(availableDevices)
if (selectedDevice) {
await installApkFiles(selectedDevice, apkFiles)
}
} catch (error) {
console.warn('Device selection cancelled')
}
}

async function installApkFiles(device, files) {
try {
// Convert File objects to file paths (this is a simplified approach)
// In a real implementation, we'd need to save the files temporarily first
const filePaths = await Promise.all(
files.map(async (file) => {
// Create a temporary file path - this would need to be implemented properly
// For now, we'll use the existing Application component's install method
return file.path || file.name
})
)

// Use the existing Application component's install logic
const applicationComponent = {
$adb: proxy.$adb,
$t: proxy.$t,
deviceStore,
handleInstall: async (device, { files, silent = false } = {}) => {
// This replicates the logic from Application/index.vue
let closeLoading = null
if (!silent) {
closeLoading = proxy.$message.loading(
proxy.$t('device.control.install.progress', {
deviceName: deviceStore.getLabel(device),
})
).close
}

let failCount = 0
const totalCount = files.length

// Install each APK file
for (const filePath of files) {
try {
await proxy.$adb.install(device.id, filePath)
} catch (e) {
console.warn(e)
failCount++
}
}

if (!silent) {
closeLoading?.()
}

const successCount = totalCount - failCount

if (successCount > 0) {
if (totalCount > 1) {
proxy.$message.success(
proxy.$t('device.control.install.success', {
deviceName: deviceStore.getLabel(device),
totalCount,
successCount,
failCount,
})
)
} else {
proxy.$message.success(
proxy.$t('device.control.install.success.single', {
deviceName: deviceStore.getLabel(device),
})
)
}
} else {
proxy.$message.warning(proxy.$t('device.control.install.error'))
}
}
}

// For drag-and-drop, we need to handle File objects differently
// This is a simplified implementation - in production you'd want to:
// 1. Save the dropped files to a temporary location
// 2. Get their actual file paths
// 3. Then call the install method with the paths

await applicationComponent.handleInstall(device, { files: files.map(f => f.path || f.name) })

} catch (error) {
console.error('Failed to install APK files:', error)
proxy.$message.error(proxy.$t('device.control.install.dragDrop.error'))
}
}

async function showDeviceSelectionDialog(devices) {
return new Promise((resolve, reject) => {
ElMessageBox({
title: proxy.$t('device.control.install.dragDrop.selectDevice'),
message: h('div', [
h('p', proxy.$t('device.control.install.dragDrop.selectDeviceMessage')),
h('div', { class: 'mt-4' },
devices.map(device =>
h('div', {
key: device.id,
class: 'p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded cursor-pointer border mb-2',
onClick: () => resolve(device)
}, [
h('div', { class: 'font-medium' }, deviceStore.getLabel(device)),
h('div', { class: 'text-sm text-gray-500' }, device.id)
])
)
)
]),
showCancelButton: true,
showConfirmButton: false,
customClass: 'device-selection-dialog'
}).catch(() => reject())
})
}

function selectable(row) {
return ['device', 'emulator'].includes(row.status)
}
Expand Down