feat: Support graphic file manager

This commit is contained in:
viarotel 2024-09-08 01:14:28 +08:00
parent 94ee0070ef
commit 815572303a
10 changed files with 366 additions and 85 deletions

View File

@ -214,10 +214,9 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
19. 灵活启动镜像 ✅
20. 批量处理 ✅
21. 计划任务 ✅
22. 对设备进行分组 🚧
23. 文件传输助手 🚧
24. 通过界面管理设备文件 🚧
25. 游戏键位映射 🚧
22. 图形化文件管理器 ✅
23. 对设备进行分组 🚧
24. 游戏键位映射 🚧
## 常见问题

View File

@ -212,10 +212,9 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
19. Flexible mirroring launch ✅
20. Batch processing ✅
21. Scheduled tasks ✅
22. Device grouping 🚧
23. File transfer assistant 🚧
24. Manage device files via interface 🚧
25. Game key mapping 🚧
22. Graphical file manager ✅
23. Device grouping 🚧
24. Game key mapping 🚧
## FAQ

View File

@ -187,28 +187,6 @@ const clearOverlayDisplayDevices = async (deviceId) => {
)
}
const push = async (
id,
filePath,
{ progress, savePath = `/sdcard/Download/${path.basename(filePath)}` } = {},
) => {
const res = await client.getDevice(id).push(filePath, savePath)
return new Promise((resolve, reject) => {
res.on('progress', (stats) => {
progress?.(stats)
})
res.on('end', () => {
resolve(savePath)
})
res.on('error', (err) => {
reject(err)
})
})
}
const watch = async (callback) => {
const tracker = await client.trackDevices()
tracker.on('add', async (ret) => {
@ -233,11 +211,12 @@ const watch = async (callback) => {
return close
}
async function getFiles(id, path) {
const value = await client.getDevice(id).readdir(path)
async function readdir(id, filePath) {
const value = await client.getDevice(id).readdir(filePath)
return value.map(item => ({
...item,
id: [filePath, item.name].join('/'),
type: item.isFile() ? 'file' : 'directory',
name: item.name,
size: formatFileSize(item.size),
@ -245,6 +224,50 @@ async function getFiles(id, path) {
}))
}
async function push(id, filePath, args = {}) {
const { progress, savePath = `/sdcard/Download/${path.basename(filePath)}` }
= args
const transfer = await client.getDevice(id).push(filePath, savePath)
return new Promise((resolve, reject) => {
transfer.on('progress', (stats) => {
progress?.(stats)
})
transfer.on('end', () => {
resolve(savePath)
})
transfer.on('error', (err) => {
reject(err)
})
})
}
async function pull(id, filePath, args = {}) {
const { progress, savePath = path.resolve('../', path.basename(filePath)) }
= args
const transfer = await client.getDevice(id).pull(filePath)
return new Promise((resolve, reject) => {
transfer.on('progress', (stats) => {
progress?.(stats)
})
transfer.on('end', () => {
resolve(savePath)
})
transfer.on('error', (err) => {
reject(err)
})
transfer.pipe(fs.createWriteStream(savePath))
})
}
export default () => {
const binPath = appStore.get('common.adbPath') || adbPath
@ -269,7 +292,8 @@ export default () => {
display,
clearOverlayDisplayDevices,
push,
pull,
watch,
getFiles,
readdir,
}
}

View File

@ -0,0 +1,46 @@
<template>
<el-popover
ref="popoverRef"
placement="bottom-start"
:width="300"
trigger="click"
@hide="onHide"
>
<template #reference>
<slot name="reference"></slot>
</template>
<div class="flex items-center space-x-2">
<el-input
v-model="dirname"
:placeholder="$t('common.input.placeholder')"
clearable
class="flex-1 w-0"
></el-input>
<el-button type="primary" class="flex-none" @click="handleConfirm">
{{ $t('common.confirm') }}
</el-button>
</div>
</el-popover>
</template>
<script setup>
const emit = defineEmits(['success'])
const defaultText = 'NewFolder'
const dirname = ref(defaultText)
const popoverRef = ref()
function onHide() {
dirname.value = defaultText
}
function handleConfirm() {
emit('success', dirname.value)
popoverRef.value.hide()
}
</script>
<style></style>

View File

@ -1,9 +1,10 @@
<template>
<el-dialog
v-model="visible"
v-model="dialog.visible"
:title="$t('device.control.file.name')"
width="97%"
append-to-body
destroy-on-close
class="el-dialog--beautify"
@closed="onClosed"
>
@ -40,88 +41,122 @@
</el-breadcrumb>
<div class="ml-auto">
<el-button text icon="Refresh" circle></el-button>
<el-button text icon="Refresh" circle @click="getTableData"></el-button>
</div>
</div>
<div class="mb-4 -ml-px">
<el-button type="default" icon="FolderAdd">
新建文件夹
<el-button-group class="mb-4 -ml-px">
<AddPopover @success="handleAdd">
<template #reference>
<el-button type="default" icon="FolderAdd">
{{ $t('device.control.file.manager.add') }}
</el-button>
</template>
</AddPopover>
<el-button
type="default"
icon="DocumentAdd"
v-bind="{ loading: fileActions.loading }"
@click="handleUpload(device)"
>
{{ $t('device.control.file.manager.upload') }}
</el-button>
<el-button type="default" icon="DocumentAdd">
上传文件
<el-button
type="default"
icon="Download"
:disabled="!selectionRows.length"
@click="handleDownload()"
>
{{ $t('device.control.file.manager.download') }}
</el-button>
<el-button type="default" icon="Download">
下载文件
</el-button>
</div>
</el-button-group>
<el-table
v-loading="loading"
:data="tableData"
stripe
size="small"
row-key="id"
@selection-change="onSelectionChange"
>
<el-table-column
type="selection"
reserve-selection
width="50"
align="left"
:selectable="(row) => ['file'].includes(row.type)"
></el-table-column>
<el-table-column prop="name" label="名称" sortable>
<el-table-column prop="name" :label="$t('common.name')" sortable>
<template #default="{ row }">
<div class="flex items-center">
<el-button
<el-link
v-if="row.type === 'directory'"
text
type="default"
icon="Folder"
class="!p-0 !bg-transparent"
class="!space-x-2"
@click="handleDirectory(row)"
>
{{ row.name }}
</el-button>
<el-button
</el-link>
<el-link
v-else
text
type="default"
icon="Document"
class="!p-0 !bg-transparent"
@click="handleFile(row)"
class="!space-x-2"
@click="handleDownload(row)"
>
{{ row.name }}
</el-button>
</el-link>
</div>
</template>
</el-table-column>
<el-table-column
prop="size"
label="大小"
:label="$t('common.size')"
sortable
align="center"
></el-table-column>
>
</el-table-column>
<el-table-column
prop="updateTime"
label="修改时间"
:label="$t('time.update')"
sortable
align="center"
></el-table-column>
>
</el-table-column>
<el-table-column label="操作" align="center">
<el-table-column :label="$t('device.control.name')" align="center">
<template #default="{ row }">
<div class="">
<EleTooltipButton
v-if="['file'].includes(row.type)"
effect="light"
placement="top"
:offset="2"
:content="$t('common.download')"
text
type="primary"
icon="Download"
circle
@click="handleFile(row)"
>
</EleTooltipButton>
</div>
<EleTooltipButton
v-if="['file'].includes(row.type)"
effect="light"
placement="top"
:offset="2"
:content="$t('common.download')"
text
type="primary"
icon="Download"
circle
@click="handleDownload(row)"
>
</EleTooltipButton>
<EleTooltipButton
effect="light"
placement="top"
:offset="2"
:content="$t('common.delete')"
text
type="danger"
icon="Delete"
circle
@click="handleRemove(row)"
>
</EleTooltipButton>
</template>
</el-table-column>
</el-table>
@ -131,7 +166,18 @@
</template>
<script setup>
const visible = ref(false)
import { ElMessageBox } from 'element-plus'
import AddPopover from './AddPopover/index.vue'
import { usePreferenceStore } from '$/store'
import { useDialog, useFileActions } from '$/composables/index.js'
const preferenceStore = usePreferenceStore()
const fileActions = reactive(useFileActions())
const dialog = reactive(useDialog())
const device = ref()
@ -145,7 +191,10 @@ const breadcrumbModel = computed(() => {
const pathList = currentPath.value.split('/')
const value = pathList.map(item => ({
label: item === 'sdcard' ? '内部存储空间' : void 0,
label:
item === 'sdcard'
? window.t('device.control.file.manager.storage')
: void 0,
value: item,
}))
@ -153,17 +202,20 @@ const breadcrumbModel = computed(() => {
})
function open(args) {
visible.value = true
device.value = args
dialog.open(args)
getTableData()
}
function onClosed() {}
function onClosed() {
currentPath.value = 'sdcard'
dialog.reset()
}
async function getTableData() {
loading.value = true
const data = await window.adbkit.getFiles(device.value.id, currentPath.value)
const data = await window.adbkit.readdir(device.value.id, currentPath.value)
loading.value = false
@ -176,8 +228,6 @@ function onSelectionChange(selection) {
selectionRows.value = selection
}
function handleFile(row) {}
function handleDirectory(row) {
currentPath.value += `/${row.name}`
getTableData()
@ -206,6 +256,84 @@ function handlePrev() {
getTableData()
}
async function handleAdd(dirname) {
await window.adbkit.deviceShell(
device.value.id,
`mkdir ${currentPath.value}/${dirname}`,
)
getTableData()
}
async function handleRemove(row) {
try {
await ElMessageBox.confirm(
window.t('device.control.file.manager.delete.tips'),
window.t('common.tips'),
{
type: 'warning',
},
)
}
catch (error) {
return error.message
}
await window.adbkit.deviceShell(
device.value.id,
`rm -r ${currentPath.value}/${row.name}`,
)
getTableData()
}
async function handleUpload() {
await fileActions.send(device.value)
getTableData()
}
async function handleDownload(row) {
try {
await ElMessageBox.confirm(
window.t('device.control.file.manager.download.tips'),
window.t('common.tips'),
{
type: 'info',
},
)
}
catch (error) {
return error.message
}
const pathList = row
? [row.id]
: selectionRows.value
.filter(item => item.type === 'file')
.map(item => item.id)
const deviceConfig = preferenceStore.getData(device.value.id)
const closeLoading = ElMessage.loading(window.t('common.downloading')).close
for (let index = 0; index < pathList.length; index++) {
const item = pathList[index]
const savePath = window.nodePath.resolve(
deviceConfig.savePath,
window.nodePath.basename(item),
)
await window.adbkit
.pull(device.value.id, item, { savePath })
.catch(e => console.warn(e?.message))
}
closeLoading()
ElMessage.success(window.t('common.success'))
}
defineExpose({
open,
})

5
src/composables/index.js Normal file
View File

@ -0,0 +1,5 @@
export { useDialog } from './useDialog/index.js'
export { useFileActions } from './useFileActions/index.js'
export { useInstallAction } from './useInstallAction/index.js'
export { useScreenshotAction } from './useScreenshotAction/index.js'
export { useShellAction } from './useShellAction/index.js'

View File

@ -0,0 +1,45 @@
import { sleep } from '$/utils'
export function useDialog() {
const visible = ref(false)
const lazyVisible = ref(false)
const loading = ref(false)
const params = ref({})
watch(
() => visible.value,
async (value) => {
if (!value) {
await sleep()
}
lazyVisible.value = value
},
)
function open(args) {
visible.value = true
params.value = args?.params ?? {}
}
function close() {
visible.value = false
}
function reset() {
visible.value = false
loading.value = false
params.value = {}
}
return {
visible,
lazyVisible,
loading,
params,
open,
close,
reset,
}
}
export default useDialog

View File

@ -19,6 +19,11 @@
"common.remove": "Remove",
"common.select.please": "Please Select",
"common.required": "This field cannot be empty",
"common.download": "Download",
"common.downloading": "Downloading",
"common.delete": "Delete",
"common.name": "Name",
"common.size": "Size",
"common.language.name": "Language",
"common.language.placeholder": "Select language",
@ -26,6 +31,7 @@
"common.language.zh-TW": "繁體中文",
"common.language.en-US": "English",
"time.update": "Update Time",
"time.unit.month": "month",
"time.unit.week": "week",
"time.unit.day": "day",
@ -134,9 +140,15 @@
"device.control.file.push.placeholder": "Please select the file to push",
"device.control.file.push.loading": "Push file...",
"device.control.file.push.success.name": "Push files successfully",
"device.control.file.push.success": "Successfully pushed {totalCount} files to the /sdcard/Download/ directory of {deviceName}, {successCount} succeeded, and {failCount} failed",
"device.control.file.push.success": "Successfully pushed {totalCount} files to {deviceName}, {successCount} succeeded, and {failCount} failed",
"device.control.file.push.success.single": "Files successfully pushed to the /sdcard/Download/ directory of {deviceName}",
"device.control.file.push.error": "Failed to push the file, please check the file and try again",
"device.control.file.manager.storage": "Internal Storage",
"device.control.file.manager.add": "New Folder",
"device.control.file.manager.upload": "Upload File",
"device.control.file.manager.download": "Download File",
"device.control.file.manager.download.tips": "Are you sure you want to download the selected content?",
"device.control.file.manager.delete.tips": "Are you sure you want to delete the selected content?",
"device.control.shell.name": "Execute Script",
"device.control.shell.tips": "Perform custom script through the ADB command",
"device.control.shell.select": "Please select the script you want to execute",

View File

@ -5,7 +5,6 @@
"common.default": "默认",
"common.tips": "提示",
"common.open": "打开",
"common.download": "下载",
"common.input.placeholder": "请填写",
"common.success": "操作成功",
"common.success.batch": "批量操作成功",
@ -20,6 +19,11 @@
"common.remove": "移除",
"common.select.please": "请选择",
"common.required": "该选项不能为空",
"common.download": "下载",
"common.downloading": "正在下载中",
"common.delete": "删除",
"common.name": "名称",
"common.size": "大小",
"common.language.name": "语言",
"common.language.placeholder": "选择你需要的语言",
@ -27,6 +31,7 @@
"common.language.zh-TW": "繁體中文",
"common.language.en-US": "English",
"time.update": "更新时间",
"time.unit.month": "月",
"time.unit.week": "周",
"time.unit.day": "天",
@ -135,9 +140,15 @@
"device.control.file.push.placeholder": "请选择要推送的文件",
"device.control.file.push.loading": "推送文件中...",
"device.control.file.push.success.name": "推送文件成功",
"device.control.file.push.success": "已成功将 {totalCount} 个文件推送到 {deviceName} 的 /sdcard/Download/ 目录{successCount} 成功,{failCount} 失败。",
"device.control.file.push.success": "已成功将 {totalCount} 个文件推送到 {deviceName}{successCount} 成功,{failCount} 失败。",
"device.control.file.push.success.single": "文件已成功推送到 {deviceName} 的 /sdcard/Download/ 目录",
"device.control.file.push.error": "推送文件失败,请检查文件后重试",
"device.control.file.manager.storage": "内部存储空间",
"device.control.file.manager.add": "新建文件夹",
"device.control.file.manager.upload": "上传文件",
"device.control.file.manager.download": "下载文件",
"device.control.file.manager.download.tips": "确定要下载所选内容吗",
"device.control.file.manager.delete.tips": "确定要删除所选内容吗",
"device.control.shell.name": "执行脚本",
"device.control.shell.tips": "通过 ADB 命令执行自定义脚本",
"device.control.shell.select": "请选择要执行的脚本",

View File

@ -19,6 +19,11 @@
"common.remove": "移除",
"common.select.please": "請選擇",
"common.required": "該選項不能為空",
"common.download": "下載",
"common.downloading": "正在下載中",
"common.delete": "刪除",
"common.name": "名稱",
"common.size": "大小",
"common.language.name": "語言",
"common.language.placeholder": "選擇你要的語言",
@ -26,6 +31,7 @@
"common.language.zh-TW": "繁體中文",
"common.language.en-US": "English",
"time.update": "更新時間",
"time.unit.month": "月",
"time.unit.week": "週",
"time.unit.day": "天",
@ -134,9 +140,15 @@
"device.control.file.push.placeholder": "請選擇要推送的檔案",
"device.control.file.push.loading": "推送檔案中...",
"device.control.file.push.success.name": "推送檔案成功",
"device.control.file.push.success": "已成功將 {totalCount} 個檔案推送到 {deviceName} 的 /sdcard/Download/ 目錄{successCount} 成功,{failCount} 失敗。",
"device.control.file.push.success": "已成功將 {totalCount} 個檔案推送到 {deviceName}{successCount} 成功,{failCount} 失敗。",
"device.control.file.push.success.single": "檔案已成功推送到 {deviceName} 的 /sdcard/Download/ 目錄",
"device.control.file.push.error": "推送檔案失敗,請檢查檔案後重試",
"device.control.file.manager.storage": "內部儲存空間",
"device.control.file.manager.add": "新增資料夾",
"device.control.file.manager.upload": "上傳檔案",
"device.control.file.manager.download": "下載檔案",
"device.control.file.manager.download.tips": "確定要下載所選內容嗎?",
"device.control.file.manager.delete.tips": "確定要刪除所選內容嗎?",
"device.control.shell.name": "執行腳本",
"device.control.shell.tips": "透過 ADB 命令執行自訂腳本",
"device.control.shell.select": "請選擇要執行的腳本",