mirror of
https://github.com/rubickCenter/rubick
synced 2026-03-10 07:52:18 +08:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2f3f347af | ||
|
|
bd2f3b6b0e | ||
|
|
4847c58bc9 | ||
|
|
6a53a8cf16 | ||
|
|
c7691992ec | ||
|
|
0d39d2b9ce | ||
|
|
a05860bc6a | ||
|
|
3b4ca0e289 | ||
|
|
b75188fdb8 | ||
|
|
bf3ae7c9ba | ||
|
|
b7b16e2f3e | ||
|
|
3fec3665b6 | ||
|
|
b7587a454f | ||
|
|
f5f3f030ce | ||
|
|
0bddb33fde | ||
|
|
9005d070aa | ||
|
|
9adfa84cab | ||
|
|
15c160e45d | ||
|
|
6bf613042a | ||
|
|
04fe2e03a6 | ||
|
|
6c0d34fc4f | ||
|
|
3fa9bb0384 | ||
|
|
791115901a | ||
|
|
a879ed6555 | ||
|
|
28b58e7976 | ||
|
|
dc54b25f84 | ||
|
|
fc51a383bf |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn
|
yarn
|
||||||
yarn global add xvfb-maybe
|
yarn global add xvfb-maybe
|
||||||
yarn global add @vue/cli
|
yarn global add @vue/cli@4.5.0 --frozen-lockfile --ignore-engines
|
||||||
- name: Build feature
|
- name: Build feature
|
||||||
run: |
|
run: |
|
||||||
cd ./feature
|
cd ./feature
|
||||||
|
|||||||
@@ -53,5 +53,8 @@
|
|||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead"
|
"not dead"
|
||||||
]
|
],
|
||||||
|
"volta": {
|
||||||
|
"node": "16.20.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "5.0.0",
|
"less-loader": "^6.2.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.1.5"
|
||||||
},
|
},
|
||||||
@@ -67,5 +67,8 @@
|
|||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead"
|
"not dead"
|
||||||
]
|
],
|
||||||
|
"volta": {
|
||||||
|
"node": "16.20.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ $t('feature.market.systemTool') }}
|
{{ $t('feature.market.systemTool') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="localPlugin">
|
||||||
|
<template #icon>
|
||||||
|
<ApiOutlined style="font-size: 16px" />
|
||||||
|
</template>
|
||||||
|
{{ $t('feature.market.localPlugin') }}
|
||||||
|
</a-menu-item>
|
||||||
<a-sub-menu class="user-info">
|
<a-sub-menu class="user-info">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<a-avatar :size="32">
|
<a-avatar :size="32">
|
||||||
@@ -82,6 +88,7 @@
|
|||||||
'tools',
|
'tools',
|
||||||
'worker',
|
'worker',
|
||||||
'system',
|
'system',
|
||||||
|
'localPlugin',
|
||||||
].includes(active[0])
|
].includes(active[0])
|
||||||
? 'container'
|
? 'container'
|
||||||
: 'more'
|
: 'more'
|
||||||
@@ -107,6 +114,7 @@ import {
|
|||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
HeartOutlined,
|
HeartOutlined,
|
||||||
BugOutlined,
|
BugOutlined,
|
||||||
|
ApiOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import localConfig from '@/confOp';
|
import localConfig from '@/confOp';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
imageTool: 'Image Tools',
|
imageTool: 'Image Tools',
|
||||||
developTool: 'Develop Tools',
|
developTool: 'Develop Tools',
|
||||||
systemTool: 'System Tools',
|
systemTool: 'System Tools',
|
||||||
|
localPlugin: 'Custom Plugins',
|
||||||
finder: {
|
finder: {
|
||||||
must: 'Necessary',
|
must: 'Necessary',
|
||||||
recommended: 'Recommended',
|
recommended: 'Recommended',
|
||||||
@@ -114,6 +115,28 @@ export default {
|
|||||||
installSuccess: '{pluginName} Install Successed!',
|
installSuccess: '{pluginName} Install Successed!',
|
||||||
refreshSuccess: '{pluginName} Refresh Successed!',
|
refreshSuccess: '{pluginName} Refresh Successed!',
|
||||||
},
|
},
|
||||||
|
localPlugin: {
|
||||||
|
title: 'Import Local Plugin',
|
||||||
|
okText: 'Import',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
importType: 'Import Type',
|
||||||
|
localImport: 'Local Import',
|
||||||
|
remoteImport: 'Remote Import',
|
||||||
|
importUrl: 'Import Url',
|
||||||
|
importUrlPlaceholder:
|
||||||
|
'Please input remote plugin config file url. Refer to the address below',
|
||||||
|
importFile: 'Import File',
|
||||||
|
importFileErrorMsg: 'Please select the plugin config file to import',
|
||||||
|
importUrlErrorMsg:
|
||||||
|
'Please input the correct remote plugin config file url',
|
||||||
|
configFetchSuccess: 'Plugin config file import success',
|
||||||
|
deleteLocalPluginSuccess: 'Plugin delete success',
|
||||||
|
deleteLocalPluginButton: 'Delete Plugin',
|
||||||
|
deleteLocalPluginConfirm: 'Confirm Delete Plugin',
|
||||||
|
deleteLocalPluginConfirmText: 'Confirm',
|
||||||
|
deleteLocalPluginCancelText: 'Cancel',
|
||||||
|
tips: 'Local Import and Remote Import both only support importing Json format configuration files. The specific content of the Json file can refer to the link: https://gitee.com/monkeyWang/rubickdatabase/raw/master/plugins/total-plugins.json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default {
|
|||||||
imageTool: '图像',
|
imageTool: '图像',
|
||||||
developTool: '开发者',
|
developTool: '开发者',
|
||||||
systemTool: '系统',
|
systemTool: '系统',
|
||||||
|
localPlugin: '自定义插件',
|
||||||
|
|
||||||
finder: {
|
finder: {
|
||||||
must: '必备',
|
must: '必备',
|
||||||
recommended: '推荐',
|
recommended: '推荐',
|
||||||
@@ -112,6 +114,26 @@ export default {
|
|||||||
installSuccess: '{pluginName}安装成功!',
|
installSuccess: '{pluginName}安装成功!',
|
||||||
refreshSuccess: '{pluginName}刷新成功!',
|
refreshSuccess: '{pluginName}刷新成功!',
|
||||||
},
|
},
|
||||||
|
localPlugin: {
|
||||||
|
title: '导入配置文件',
|
||||||
|
okText: '导入',
|
||||||
|
cancelText: '关闭',
|
||||||
|
importType: '导入方式',
|
||||||
|
localImport: '本地导入',
|
||||||
|
remoteImport: '远程导入',
|
||||||
|
importUrl: '导入地址',
|
||||||
|
importUrlPlaceholder: '请输入远程插件配置文件地址。参考下面的地址',
|
||||||
|
importFile: '导入文件',
|
||||||
|
importFileErrorMsg: '请选择要导入的插件配置文件',
|
||||||
|
importUrlErrorMsg: '请输入正确的远程插件配置文件地址',
|
||||||
|
configFetchSuccess: '插件配置文件导入成功',
|
||||||
|
deleteLocalPluginSuccess: '插件删除成功',
|
||||||
|
deleteLocalPluginButton: '删除插件',
|
||||||
|
deleteLocalPluginConfirm: '确认删除插件',
|
||||||
|
deleteLocalPluginConfirmText: '确认',
|
||||||
|
deleteLocalPluginCancelText: '取消',
|
||||||
|
tips: '本地导入和远程导入都只支持导入Json 格式的配置文件。Json 文件的具体内容参考链接: https://gitee.com/monkeyWang/rubickdatabase/raw/master/plugins/total-plugins.json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,15 +11,18 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Radio,
|
Radio,
|
||||||
|
Typography,
|
||||||
Select,
|
Select,
|
||||||
Switch,
|
Switch,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Popconfirm,
|
||||||
Collapse,
|
Collapse,
|
||||||
List,
|
List,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Alert,
|
Alert,
|
||||||
Drawer,
|
Drawer,
|
||||||
Modal,
|
Modal,
|
||||||
|
Upload,
|
||||||
Result,
|
Result,
|
||||||
Spin,
|
Spin,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
@@ -73,6 +76,9 @@ createApp(App)
|
|||||||
.use(Modal)
|
.use(Modal)
|
||||||
.use(Result)
|
.use(Result)
|
||||||
.use(Spin)
|
.use(Spin)
|
||||||
|
.use(Upload)
|
||||||
|
.use(Popconfirm)
|
||||||
|
.use(Typography)
|
||||||
.use(router)
|
.use(router)
|
||||||
.use(Vue3Lottie)
|
.use(Vue3Lottie)
|
||||||
.mount('#app');
|
.mount('#app');
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'system',
|
name: 'system',
|
||||||
component: () => import('../views/market/components/system.vue'),
|
component: () => import('../views/market/components/system.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/localPlugin',
|
||||||
|
name: 'localPlugin',
|
||||||
|
component: () => import('../views/market/components/local-plugin.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/finder',
|
path: '/finder',
|
||||||
name: 'finder',
|
name: 'finder',
|
||||||
@@ -56,6 +61,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'dev',
|
name: 'dev',
|
||||||
component: () => import('../views/dev/index.vue'),
|
component: () => import('../views/dev/index.vue'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)',
|
path: '/:catchAll(.*)',
|
||||||
name: 'finder',
|
name: 'finder',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const isDownload = (item: Market.Plugin, targets: any[]) => {
|
|||||||
});
|
});
|
||||||
return isDownload;
|
return isDownload;
|
||||||
};
|
};
|
||||||
|
const LOCAL_PLUGIN_JSON = 'localPluginJson';
|
||||||
export default createStore({
|
export default createStore({
|
||||||
state: {
|
state: {
|
||||||
totalPlugins: [],
|
totalPlugins: [],
|
||||||
@@ -30,8 +30,25 @@ export default createStore({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
async saveLocalPlugins({ dispatch, state }, plugins) {
|
||||||
|
// 先移除
|
||||||
|
window.rubick.db.remove(LOCAL_PLUGIN_JSON);
|
||||||
|
window.rubick.db.put({
|
||||||
|
_id: LOCAL_PLUGIN_JSON,
|
||||||
|
data: JSON.stringify(plugins),
|
||||||
|
});
|
||||||
|
await dispatch('init');
|
||||||
|
},
|
||||||
|
async deleteLocalPlugins({ dispatch, state }) {
|
||||||
|
// 先移除
|
||||||
|
window.rubick.db.remove(LOCAL_PLUGIN_JSON);
|
||||||
|
await dispatch('init');
|
||||||
|
},
|
||||||
async init({ commit }) {
|
async init({ commit }) {
|
||||||
const totalPlugins = await request.getTotalPlugins();
|
const tPlugins = await request.getTotalPlugins();
|
||||||
|
const lTPlugins = window.rubick.db.get(LOCAL_PLUGIN_JSON);
|
||||||
|
const totalPlugins = tPlugins.concat(JSON.parse(lTPlugins?.data || '[]'));
|
||||||
|
|
||||||
const localPlugins = window.market.getLocalPlugins();
|
const localPlugins = window.market.getLocalPlugins();
|
||||||
|
|
||||||
totalPlugins.forEach((origin: Market.Plugin) => {
|
totalPlugins.forEach((origin: Market.Plugin) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const data = ref([]);
|
|||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
data.value = await request.getImageDetail();
|
data.value = await request.getImageDetail();
|
||||||
|
console.log(data.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const system = computed(() => {
|
const system = computed(() => {
|
||||||
|
|||||||
196
feature/src/views/market/components/local-plugin.vue
Normal file
196
feature/src/views/market/components/local-plugin.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<div class="local-plugin">
|
||||||
|
<div class="local-plugin__header">
|
||||||
|
<h3 class="local-plugin__title">
|
||||||
|
{{ $t('feature.market.localPlugin') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="local-plugin__action">
|
||||||
|
<a-button type="primary" @click="visible = true">
|
||||||
|
<template #icon><UploadOutlined /></template>
|
||||||
|
{{ $t('feature.localPlugin.title') }}
|
||||||
|
</a-button>
|
||||||
|
|
||||||
|
<a-popconfirm
|
||||||
|
placement="leftBottom"
|
||||||
|
:title="$t('feature.localPlugin.deleteLocalPluginConfirm')"
|
||||||
|
:ok-text="$t('feature.localPlugin.deleteLocalPluginConfirmText')"
|
||||||
|
:cancel-text="$t('feature.localPlugin.deleteLocalPluginCancelText')"
|
||||||
|
@confirm="handleDeleteClick"
|
||||||
|
>
|
||||||
|
<a-button type="danger">
|
||||||
|
<template #icon><DeleteOutlined /></template>
|
||||||
|
{{ $t('feature.localPlugin.deleteLocalPluginButton') }}
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="local-plugin__list">
|
||||||
|
<PluginList
|
||||||
|
v-if="pluginList && !!pluginList.length"
|
||||||
|
@downloadSuccess="downloadSuccess"
|
||||||
|
:list="pluginList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="visible"
|
||||||
|
destroy-on-close
|
||||||
|
:title="$t('feature.localPlugin.title')"
|
||||||
|
:ok-text="$t('feature.localPlugin.okText')"
|
||||||
|
:cancel-text="$t('feature.localPlugin.cancelText')"
|
||||||
|
@ok="handleOk"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<a-form>
|
||||||
|
<a-alert :message="$t('feature.localPlugin.tips')" type="success" />
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
:label="$t('feature.localPlugin.importType')"
|
||||||
|
name="importType"
|
||||||
|
>
|
||||||
|
<a-radio-group v-model:value="formState.importType">
|
||||||
|
<a-radio :value="1">
|
||||||
|
{{ $t('feature.localPlugin.localImport') }}
|
||||||
|
</a-radio>
|
||||||
|
<a-radio :value="2">
|
||||||
|
{{ $t('feature.localPlugin.remoteImport') }}
|
||||||
|
</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
:label="$t('feature.localPlugin.localImport')"
|
||||||
|
v-if="formState.importType == '1'"
|
||||||
|
name="importFile"
|
||||||
|
>
|
||||||
|
<a-upload
|
||||||
|
:maxCount="1"
|
||||||
|
accept=".json"
|
||||||
|
v-model:file-list="formState.importFile"
|
||||||
|
:beforeUpload="() => false"
|
||||||
|
name="file"
|
||||||
|
>
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon><UploadOutlined /></template>
|
||||||
|
{{ $t('feature.localPlugin.importFile') }}
|
||||||
|
</a-button>
|
||||||
|
</a-upload>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
:label="$t('feature.localPlugin.remoteImport')"
|
||||||
|
v-if="formState.importType == '2'"
|
||||||
|
name="importUrl"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:value="formState.importUrl"
|
||||||
|
:placeholder="$t('feature.localPlugin.importUrlPlaceholder')"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { UploadOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { ref, reactive, getCurrentInstance, computed } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import PluginList from './plugin-list.vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const store = useStore();
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
const pluginList = computed(() => {
|
||||||
|
return store.state.totalPlugins.filter((i) => i['#type'] == 'localPlugin');
|
||||||
|
});
|
||||||
|
const formState = reactive({
|
||||||
|
importType: 1,
|
||||||
|
importFile: [],
|
||||||
|
importUrl: '',
|
||||||
|
});
|
||||||
|
const handleOk = () => {
|
||||||
|
if (formState.importType == 1 && !formState.importFile.length) {
|
||||||
|
message.error(proxy.$t('feature.localPlugin.importFileErrorMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formState.importType == 2 && !formState.importUrl) {
|
||||||
|
message.error(proxy.$t('feature.localPlugin.importUrlErrorMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formState.importType == 1) {
|
||||||
|
readPlguinJsonByFile();
|
||||||
|
} else {
|
||||||
|
readPlguinJsonByUrl();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const readPlguinJsonByFile = () => {
|
||||||
|
const file = formState.importFile[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file.originFileObj);
|
||||||
|
reader.onload = () => {
|
||||||
|
const json = JSON.parse(reader.result);
|
||||||
|
configFetchSuccess(json);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const readPlguinJsonByUrl = () => {
|
||||||
|
fetch(formState.importUrl)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
configFetchSuccess(json);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// const json = window.rubick.db.get(LOCAL_PLUGIN_JSON);
|
||||||
|
// pluginList.value = JSON.parse(json.data);
|
||||||
|
// });
|
||||||
|
const configFetchSuccess = (json) => {
|
||||||
|
// 打上标记,表示本地插件
|
||||||
|
const plugins = json.map((i) => ({ ...i, '#type': 'localPlugin' }));
|
||||||
|
message.success(proxy.$t('feature.localPlugin.configFetchSuccess'));
|
||||||
|
visible.value = false;
|
||||||
|
formState.importFile = [];
|
||||||
|
formState.importUrl = '';
|
||||||
|
formState.importType = 1;
|
||||||
|
store.dispatch('saveLocalPlugins', plugins);
|
||||||
|
};
|
||||||
|
const handleCancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
formState.importFile = [];
|
||||||
|
formState.importUrl = '';
|
||||||
|
formState.importType = 1;
|
||||||
|
};
|
||||||
|
const handleDeleteClick = async () => {
|
||||||
|
await store.dispatch('deleteLocalPlugins');
|
||||||
|
message.success(proxy.$t('feature.localPlugin.deleteLocalPluginSuccess'));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.local-plugin {
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
&__list {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
&__title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
&__action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,7 +7,9 @@ module.exports = {
|
|||||||
// 向预处理器 Loader 传递配置选项
|
// 向预处理器 Loader 传递配置选项
|
||||||
less: {
|
less: {
|
||||||
// 配置less(其他样式解析用法一致)
|
// 配置less(其他样式解析用法一致)
|
||||||
javascriptEnabled: true, // 设置为true
|
lessOptions: {
|
||||||
|
javascriptEnabled: true, // 设置为true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,5 +39,8 @@
|
|||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead",
|
"not dead",
|
||||||
"not ie 11"
|
"not ie 11"
|
||||||
]
|
],
|
||||||
|
"volta": {
|
||||||
|
"node": "16.20.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rubick",
|
"name": "rubick",
|
||||||
"version": "4.3.5",
|
"version": "4.3.8",
|
||||||
"author": "muwoo <2424880409@qq.com>",
|
"author": "muwoo <2424880409@qq.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -79,5 +79,8 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"vue-cli-plugin-electron-builder/electron-builder": "^23.0.3",
|
"vue-cli-plugin-electron-builder/electron-builder": "^23.0.3",
|
||||||
"leveldown": "6.0.3"
|
"leveldown": "6.0.3"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "16.20.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ window.rubick = {
|
|||||||
openPlugin(plugin) {
|
openPlugin(plugin) {
|
||||||
ipcSendSync('loadPlugin', plugin);
|
ipcSendSync('loadPlugin', plugin);
|
||||||
},
|
},
|
||||||
|
onShow(cb) {
|
||||||
|
typeof cb === 'function' && (window.rubick.hooks.onShow = cb);
|
||||||
|
},
|
||||||
|
onHide(cb) {
|
||||||
|
typeof cb === 'function' && (window.rubick.hooks.onHide = cb);
|
||||||
|
},
|
||||||
// 窗口交互
|
// 窗口交互
|
||||||
hideMainWindow() {
|
hideMainWindow() {
|
||||||
ipcSendSync('hideMainWindow');
|
ipcSendSync('hideMainWindow');
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ class AdapterHandler {
|
|||||||
fs.mkdirsSync(options.baseDir);
|
fs.mkdirsSync(options.baseDir);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
`${options.baseDir}/package.json`,
|
`${options.baseDir}/package.json`,
|
||||||
'{"dependencies":{}}'
|
// '{"dependencies":{}}'
|
||||||
|
// fix 插件安装时node版本问题
|
||||||
|
JSON.stringify({
|
||||||
|
dependencies: {},
|
||||||
|
volta: {
|
||||||
|
node: '16.19.1',
|
||||||
|
},
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.baseDir = options.baseDir;
|
this.baseDir = options.baseDir;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
win.on('show', () => {
|
win.on('show', () => {
|
||||||
|
// 触发主窗口的 onShow hook
|
||||||
win.webContents.executeJavaScript(
|
win.webContents.executeJavaScript(
|
||||||
`window.rubick && window.rubick.hooks && typeof window.rubick.hooks.onShow === "function" && window.rubick.hooks.onShow()`
|
`window.rubick && window.rubick.hooks && typeof window.rubick.hooks.onShow === "function" && window.rubick.hooks.onShow()`
|
||||||
);
|
);
|
||||||
@@ -67,6 +68,7 @@ export default () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
win.on('hide', () => {
|
win.on('hide', () => {
|
||||||
|
// 触发主窗口的 onHide hook
|
||||||
win.webContents.executeJavaScript(
|
win.webContents.executeJavaScript(
|
||||||
`window.rubick && window.rubick.hooks && typeof window.rubick.hooks.onHide === "function" && window.rubick.hooks.onHide()`
|
`window.rubick && window.rubick.hooks && typeof window.rubick.hooks.onHide === "function" && window.rubick.hooks.onHide()`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const init = (plugin, window: BrowserWindow) => {
|
const init = (plugin, window: BrowserWindow) => {
|
||||||
if (view === null || view === undefined) {
|
if (view === null || view === undefined || view.inDetach) {
|
||||||
createView(plugin, window);
|
createView(plugin, window);
|
||||||
// if (viewInstance.getView(plugin.name) && !commonConst.dev()) {
|
// if (viewInstance.getView(plugin.name) && !commonConst.dev()) {
|
||||||
// view = viewInstance.getView(plugin.name).view;
|
// view = viewInstance.getView(plugin.name).view;
|
||||||
@@ -176,14 +176,29 @@ export default () => {
|
|||||||
const removeView = (window: BrowserWindow) => {
|
const removeView = (window: BrowserWindow) => {
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
executeHooks('PluginOut', null);
|
executeHooks('PluginOut', null);
|
||||||
|
// 先记住这次要移除的视图,防止后面异步代码里全局引用被换掉
|
||||||
|
const snapshotView = view;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.removeBrowserView(view);
|
// 获取当前视图,判断是否已经换成了新视图
|
||||||
if (!view.inDetach) {
|
const currentView = window.getBrowserView?.();
|
||||||
window.setBrowserView(null);
|
window.removeBrowserView(snapshotView);
|
||||||
view.webContents?.destroy();
|
|
||||||
|
// 主窗口的插件视图仍然挂着旧实例时,需要还原主窗口 UI
|
||||||
|
if (!snapshotView.inDetach) {
|
||||||
|
// 如果窗口还挂着旧视图,说明还没换掉,需要把主窗口恢复到初始状态
|
||||||
|
if (currentView === snapshotView) {
|
||||||
|
window.setBrowserView(null);
|
||||||
|
if (view === snapshotView) {
|
||||||
|
window.webContents?.executeJavaScript(`window.initRubick()`);
|
||||||
|
view = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshotView.webContents?.destroy();
|
||||||
|
}
|
||||||
|
// 分离窗口只需释放全局引用,视图由分离窗口继续管理
|
||||||
|
else if (view === snapshotView) {
|
||||||
|
view = undefined;
|
||||||
}
|
}
|
||||||
window.webContents?.executeJavaScript(`window.initRubick()`);
|
|
||||||
view = undefined;
|
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,30 @@ import DBInstance from './db';
|
|||||||
import getWinPosition from './getWinPosition';
|
import getWinPosition from './getWinPosition';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import commonConst from '@/common/utils/commonConst';
|
import commonConst from '@/common/utils/commonConst';
|
||||||
|
import { copyFilesToWindowsClipboard } from './windowsClipboard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sanitize input files 剪贴板文件合法性校验
|
||||||
|
* @param input
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const sanitizeInputFiles = (input: unknown): string[] => {
|
||||||
|
const candidates = Array.isArray(input)
|
||||||
|
? input
|
||||||
|
: typeof input === 'string'
|
||||||
|
? [input]
|
||||||
|
: [];
|
||||||
|
return candidates
|
||||||
|
.map((filePath) => (typeof filePath === 'string' ? filePath.trim() : ''))
|
||||||
|
.filter((filePath) => {
|
||||||
|
if (!filePath) return false;
|
||||||
|
try {
|
||||||
|
return fs.existsSync(filePath);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const runnerInstance = runner();
|
const runnerInstance = runner();
|
||||||
const detachInstance = detach();
|
const detachInstance = detach();
|
||||||
@@ -44,6 +68,20 @@ class API extends DBInstance {
|
|||||||
mainWindow.webContents.on('before-input-event', (event, input) =>
|
mainWindow.webContents.on('before-input-event', (event, input) =>
|
||||||
this.__EscapeKeyDown(event, input, mainWindow)
|
this.__EscapeKeyDown(event, input, mainWindow)
|
||||||
);
|
);
|
||||||
|
// 设置主窗口的 show/hide 事件监听
|
||||||
|
this.setupMainWindowHooks(mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMainWindowHooks(mainWindow: BrowserWindow) {
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
// 触发插件的 onShow hook
|
||||||
|
runnerInstance.executeHooks('Show', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('hide', () => {
|
||||||
|
// 触发插件的 onHide hook
|
||||||
|
runnerInstance.executeHooks('Hide', null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentWindow = (window, e) => {
|
public getCurrentWindow = (window, e) => {
|
||||||
@@ -230,13 +268,28 @@ class API extends DBInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyFile({ data }) {
|
public copyFile({ data }) {
|
||||||
if (data.file && fs.existsSync(data.file)) {
|
const targetFiles = sanitizeInputFiles(data?.file);
|
||||||
clipboard.writeBuffer(
|
|
||||||
'NSFilenamesPboardType',
|
if (!targetFiles.length) {
|
||||||
Buffer.from(plist.build([data.file]))
|
return false;
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
try {
|
||||||
|
clipboard.writeBuffer(
|
||||||
|
'NSFilenamesPboardType',
|
||||||
|
Buffer.from(plist.build(targetFiles))
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return copyFilesToWindowsClipboard(targetFiles);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ const registerHotKey = (mainWindow: BrowserWindow): void => {
|
|||||||
// 显示主窗口
|
// 显示主窗口
|
||||||
function mainWindowPopUp() {
|
function mainWindowPopUp() {
|
||||||
const currentShow = mainWindow.isVisible() && mainWindow.isFocused();
|
const currentShow = mainWindow.isVisible() && mainWindow.isFocused();
|
||||||
if (currentShow) return mainWindow.hide();
|
if (currentShow) {
|
||||||
|
mainWindow.blur(); // 先失去焦点,使焦点恢复到之前的应用程序
|
||||||
|
mainWindow.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { x: wx, y: wy } = winPosition.getPosition();
|
const { x: wx, y: wy } = winPosition.getPosition();
|
||||||
mainWindow.setAlwaysOnTop(false);
|
mainWindow.setAlwaysOnTop(false);
|
||||||
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
|||||||
133
src/main/common/windowsClipboard.ts
Normal file
133
src/main/common/windowsClipboard.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { clipboard } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// 仅在 Windows 平台辅助操作剪贴板多文件格式。
|
||||||
|
type ClipboardExModule = typeof import('electron-clipboard-ex');
|
||||||
|
|
||||||
|
const DROPFILES_HEADER_SIZE = 20;
|
||||||
|
|
||||||
|
let clipboardExModule: ClipboardExModule | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows 平台专用:尝试加载第三方库 electron-clipboard-ex。
|
||||||
|
* 这个库能够调用系统底层接口写入“文件复制”数据,成功率更高。
|
||||||
|
* 其他系统无需加载它,因此这里做了“按需加载”的处理。
|
||||||
|
*/
|
||||||
|
const ensureClipboardEx = (): ClipboardExModule | null => {
|
||||||
|
if (process.platform !== 'win32') return null;
|
||||||
|
if (clipboardExModule) return clipboardExModule;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||||
|
clipboardExModule = require('electron-clipboard-ex');
|
||||||
|
} catch {
|
||||||
|
clipboardExModule = null;
|
||||||
|
}
|
||||||
|
return clipboardExModule;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把一组文件路径变成 Windows 规定的文本格式。
|
||||||
|
* 要求:每个路径之间用单个空字符分隔,最后再额外放两个空字符,表示列表结束。
|
||||||
|
* Windows 资源管理器会按这个格式解析我们复制到剪贴板的文件。
|
||||||
|
*/
|
||||||
|
const buildWindowsFileListPayload = (files: string[]): Buffer =>
|
||||||
|
Buffer.from(`${files.join('\0')}\0\0`, 'utf16le');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 CF_HDROP 专用的二进制数据。
|
||||||
|
* 这是 Windows 复制文件时的底层格式,前 20 字节是固定的结构头,
|
||||||
|
* 后面紧跟着具体的文件路径(由 buildWindowsFileListPayload 生成)。
|
||||||
|
* 只要把这个内容写入剪贴板,任何支持粘贴文件的程序都能理解。
|
||||||
|
*/
|
||||||
|
const buildWindowsFileDropBuffer = (files: string[]): Buffer => {
|
||||||
|
const payload = buildWindowsFileListPayload(files);
|
||||||
|
const header = Buffer.alloc(DROPFILES_HEADER_SIZE);
|
||||||
|
header.writeUInt32LE(DROPFILES_HEADER_SIZE, 0);
|
||||||
|
header.writeInt32LE(0, 4);
|
||||||
|
header.writeInt32LE(0, 8);
|
||||||
|
header.writeUInt32LE(0, 12);
|
||||||
|
header.writeUInt32LE(1, 16);
|
||||||
|
|
||||||
|
const result = Buffer.alloc(header.length + payload.length);
|
||||||
|
for (let i = 0; i < header.length; i += 1) {
|
||||||
|
result[i] = header[i];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < payload.length; i += 1) {
|
||||||
|
result[header.length + i] = payload[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制/移动/创建快捷方式 等不同操作在 Windows 中对应不同的“意图”值。
|
||||||
|
* Preferred DropEffect 告诉系统:当前剪贴板数据应该以何种方式处理。
|
||||||
|
* 我们默认写入“copy”,相当于普通的复制粘贴。
|
||||||
|
*/
|
||||||
|
const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => {
|
||||||
|
const effectMap = {
|
||||||
|
copy: 1,
|
||||||
|
move: 2,
|
||||||
|
link: 4,
|
||||||
|
} as const;
|
||||||
|
const buffer = Buffer.alloc(4);
|
||||||
|
buffer.writeUInt32LE(effectMap[effect], 0);
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接使用 Electron 内置 API 写入多种剪贴板格式。
|
||||||
|
* 步骤:
|
||||||
|
* 1. 写入二进制的 CF_HDROP(含头部与路径列表)
|
||||||
|
* 2. 写入纯文本形式的 FileNameW(备选格式)
|
||||||
|
* 3. 写入 Preferred DropEffect(告诉系统“这是复制”)
|
||||||
|
* 全部成功后,读取一次 CF_HDROP 的长度,确认剪贴板里确实有内容。
|
||||||
|
*/
|
||||||
|
const writeWindowsBuffers = (files: string[]): boolean => {
|
||||||
|
try {
|
||||||
|
clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files));
|
||||||
|
clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files));
|
||||||
|
clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy'));
|
||||||
|
return clipboard.readBuffer('CF_HDROP').length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果项目中安装了 electron-clipboard-ex,我们优先使用它。
|
||||||
|
* 理由:该库通过原生方式与系统交互,兼容性往往优于 Electron 的 JS 层写入。
|
||||||
|
* 调用成功后,必要时读回文件列表做一次数量校验,确保复制的文件数量正确。
|
||||||
|
*/
|
||||||
|
const writeWithClipboardEx = (files: string[]): boolean => {
|
||||||
|
const clipboardEx = ensureClipboardEx();
|
||||||
|
if (!clipboardEx) return false;
|
||||||
|
try {
|
||||||
|
clipboardEx.writeFilePaths(files);
|
||||||
|
if (typeof clipboardEx.readFilePaths === 'function') {
|
||||||
|
const result = clipboardEx.readFilePaths();
|
||||||
|
return Array.isArray(result) && result.length === files.length;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外暴露的唯一入口。
|
||||||
|
* 1. 先把所有路径换成 Windows 可识别的标准形式(path.normalize)。
|
||||||
|
* 2. 尝试使用 electron-clipboard-ex 写入,如果成功就结束。
|
||||||
|
* 3. 若第三方库不可用或失败,再退回 Electron 原生写入流程。
|
||||||
|
* 这一层屏蔽了所有细节,外部调用者只需传入字符串数组即可。
|
||||||
|
*/
|
||||||
|
export const copyFilesToWindowsClipboard = (files: string[]): boolean => {
|
||||||
|
const normalizedFiles = files
|
||||||
|
.map((filePath) => path.normalize(filePath))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!normalizedFiles.length) return false;
|
||||||
|
if (writeWithClipboardEx(normalizedFiles)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return writeWindowsBuffers(normalizedFiles);
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user