Compare commits

...

6 Commits

Author SHA1 Message Date
木偶
c99a3eb74c Merge pull request #243 from rubickCenter/feat/v3.0.0
 支持 mac M1 arm 64:#207,#134; 支持右击菜单
2023-09-22 14:50:07 +08:00
muwoo
32c6cf5d1c 支持 mac M1 arm 64:#207,#134; 支持右击菜单 2023-09-22 14:42:35 +08:00
木偶
978b48264f Merge pull request #242 from rubickCenter/feat/v3.0.0
🐛 #215,#237
2023-09-22 10:10:25 +08:00
muwoo
79e39018fd 🐛 #215,#237 2023-09-22 10:08:27 +08:00
木偶
fe159427b8 Merge pull request #235 from rubickCenter/feat/v3.0.0
 支持 webdav 数据多端同步
2023-09-20 11:19:41 +08:00
muwoo
b94d725d69 支持 webdav 数据多端同步 2023-09-20 10:25:41 +08:00
26 changed files with 1699 additions and 145 deletions

View File

@@ -36,4 +36,11 @@ window.market = {
removeLocalStartPlugin(plugin) {
ipcSend('removeLocalStartPlugin', { plugin });
},
dbDump(target) {
ipcSend('dbDump', { target });
},
dbImport(target) {
ipcSend('dbImport', { target });
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -99,6 +99,9 @@ export default {
localstart: {
title: 'Local Start',
},
database: {
title: 'Data Synchronization',
},
},
dev: {
title: 'Developer',

View File

@@ -97,6 +97,9 @@ export default {
localstart: {
title: '本地启动',
},
database: {
title: '多端数据同步',
},
},
dev: {
title: '开发者',

View File

@@ -0,0 +1,268 @@
<template>
<div class="export-header">
<a-button @click="exportData" size="small" type="primary" style="margin-right: 10px;">
导出数据
<template #icon>
<ExportOutlined />
</template>
</a-button>
<a-button @click="importData" danger size="small" style="margin-right: 10px;">
导入数据
<template #icon>
<ImportOutlined />
</template>
</a-button>
<a-button type="link" size="small" @click="showSetting = true">
<template #icon>
<SettingOutlined />
</template>
</a-button>
</div>
<a-list item-layout="horizontal" :data-source="dataPlugins">
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a v-if="!item.plugin.isdownload && !item.plugin.isloading" key="list-loadmore-edit"
@click="() => downloadPlugin(item.plugin)">
<CloudDownloadOutlined style="font-size: 18px;"/>
</a>
<a v-if="item.plugin.isloading" key="list-loadmore-edit">
<LoadingOutlined style="font-size: 18px;"/>
</a>
<a key="list-loadmore-edit" @click="() => showKeys(item)">
<DatabaseOutlined style="font-size: 18px;"/>
</a>
</template>
<a-list-item-meta :description="`${item.keys.length} 份文档`">
<template #title>
<div>
<span>{{ item.plugin.pluginName }}</span>
</div>
</template>
<template #avatar>
<a-avatar shape="square" :src="item.plugin.logo"/>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<a-drawer
v-model:visible="open"
width="400"
:closable="false"
:title="currentSelect.plugin.pluginName"
placement="right"
>
<p
class="key-item"
:key="key"
@click="() => showDetail(key)"
v-for="key in currentSelect.keys"
>
{{ key }}
</p>
</a-drawer>
<a-modal
centered
bodyStyle="max-height: 500px; overflow: auto"
:footer="null"
:closable="false"
v-model:visible="show"
>
<pre>{{ JSON.stringify(detail, null, 2) }}</pre>
</a-modal>
<a-modal
v-model:visible="showSetting"
title="webdav 账户配置"
:footer="null"
>
<a-alert v-if="formState.suport === 'jianguo'" style="margin-bottom: 20px;" type="info" show-icon>
<template #message>
<div
@click="openHelp"
class="openHelp">
点击查看如何获取坚果云账号密码
</div>
</template>
</a-alert>
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="handleOk"
>
<a-form-item
label="webdav 提供商"
name="suport"
>
<a-select v-model:value="formState.suport">
<a-select-option value="jianguo">坚果云</a-select-option>
<a-select-option value="auto">自定义</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="服务器地址"
name="url"
v-show="formState.suport === 'auto'"
:rules="[{ required: true, message: '请填写服务器地址!' }]"
>
<a-input v-model:value="formState.url" />
</a-form-item>
<a-form-item
label="账户"
name="username"
:rules="[{ required: true, message: '请填写 username!' }]"
>
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请填写 password!' }]"
>
<a-input-password v-model:value="formState.password" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">保存设置</a-button>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { useStore } from 'vuex';
import { computed, ref, reactive } from 'vue';
import {
DatabaseOutlined,
CloudDownloadOutlined,
LoadingOutlined,
ExportOutlined,
ImportOutlined,
SettingOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const open = ref(false);
const show = ref(false);
const showSetting = ref(false);
const currentSelect = ref({ plugin: {} });
const detail = ref({});
const defaultConfig = window.rubick.dbStorage.getItem(
'rubick-db-jg-webdav'
) || {
url: 'https://dav.jianguoyun.com/dav/',
username: '',
password: '',
};
if (!defaultConfig.suport) {
defaultConfig.suport = 'jianguo';
}
const formState = reactive(defaultConfig);
const showKeys = (item) => {
open.value = true;
currentSelect.value = item;
};
const handleOk = () => {
window.rubick.dbStorage.setItem(
'rubick-db-jg-webdav',
JSON.parse(JSON.stringify(formState))
);
message.success('保存成功');
showSetting.value = false;
};
const showDetail = (key) => {
show.value = true;
detail.value = window.rubick.db.get(key);
};
const exportData = () => {
if (!formState.password || !formState.username) {
return showSetting.value = true;
}
window.market.dbDump(JSON.parse(JSON.stringify(formState)));
};
const importData = () => {
if (!formState.password || !formState.username) {
return showSetting.value = true;
}
Modal.confirm({
title: '导入确认',
content: '导入坚果云数据将会覆盖本地数据,是否确认导入?',
onOk() {
window.market.dbImport(JSON.parse(JSON.stringify(formState)));
},
});
};
const openHelp = () => {
window.rubick.shellOpenExternal('https://help.jianguoyun.com/?p=2064');
};
const store = useStore();
const pluginsData = window.rubick.db.get('RUBICK_PLUGIN_INFO');
const totalPlugins = computed(() => store.state.totalPlugins);
const dataPlugins = computed(() => {
if (!pluginsData) return [];
return pluginsData.data.map((item) => {
let plugin = null;
if (['rubick-system-feature'].includes(item.name)) {
plugin = {
pluginName: '主程序',
isdownload: true,
logo: require('../../assets/logo.png'),
};
} else {
plugin = totalPlugins.value.find((p) => p.name === item.name);
}
const data = item.keys.map((key) => window.rubick.db.get(key));
return {
...item,
plugin,
data,
};
});
});
const startDownload = (name) => store.dispatch('startDownload', name);
const successDownload = (name) => store.dispatch('successDownload', name);
const downloadPlugin = async (plugin) => {
startDownload(plugin.name);
await window.market.downloadPlugin(plugin);
message.success(t('feature.dev.installSuccess', { pluginName: plugin.name }));
successDownload(plugin.name);
};
</script>
<style lang="less">
.openHelp {
cursor: pointer;
text-decoration: underline;
}
.export-header {
width: 100%;
height: 40px;
border-bottom: 1px solid #eee;
text-align: right;
}
.key-item {
cursor: pointer;
&:hover {
color: var(--ant-primary-color);
}
}
</style>

View File

@@ -32,10 +32,16 @@
<!-- </template>-->
<!-- {{ $t('feature.settings.superPanel.title') }}-->
<!-- </a-menu-item>-->
<a-menu-item key="localhost">
<a-menu-item key="database">
<template #icon>
<DatabaseOutlined />
</template>
{{ $t('feature.settings.database.title') }}
</a-menu-item>
<a-menu-item key="localhost">
<template #icon>
<SafetyOutlined />
</template>
{{ $t('feature.settings.intranet.title') }}
</a-menu-item>
</a-menu>
@@ -227,6 +233,7 @@
<SuperPanel v-if="currentSelect[0] === 'superpanel'" />
<Localhost v-if="currentSelect[0] === 'localhost'" />
<LocalStart v-if="currentSelect[0] === 'localstart'" />
<DataBase v-if="currentSelect[0] === 'database'" />
</div>
</div>
</template>
@@ -240,6 +247,7 @@ import {
PlusCircleOutlined,
UserOutlined,
FolderOpenOutlined,
SafetyOutlined,
} from '@ant-design/icons-vue';
import debounce from 'lodash.debounce';
import { ref, reactive, watch, toRefs, computed } from 'vue';
@@ -248,6 +256,7 @@ import Localhost from './localhost.vue';
import SuperPanel from './super-panel.vue';
import UserInfo from './user-info';
import LocalStart from './local-start';
import DataBase from './database';
import { useI18n } from 'vue-i18n';
import localConfig from '@/confOp';

View File

@@ -1,6 +1,6 @@
{
"name": "rubick",
"version": "3.1.1",
"version": "3.2.2",
"author": "muwoo <2424880409@qq.com>",
"private": true,
"scripts": {
@@ -31,13 +31,17 @@
"get-mac-apps": "^1.0.2",
"got": "^11.8.3",
"lodash.throttle": "^4.1.1",
"memorystream": "^0.3.1",
"node-key-sender": "^1.0.11",
"pinyin-match": "^1.2.4",
"pouchdb": "^7.2.2",
"pouchdb-load": "^1.4.6",
"pouchdb-replication-stream": "^1.2.9",
"simple-plist": "0.2.1",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0",
"webdav": "4.11.3",
"worker-loader": "^3.0.8"
},
"devDependencies": {

View File

@@ -36,4 +36,11 @@ window.market = {
removeLocalStartPlugin(plugin) {
ipcSend('removeLocalStartPlugin', { plugin });
},
dbDump(target) {
ipcSend('dbDump', { target });
},
dbImport(target) {
ipcSend('dbImport', { target });
},
};

13
public/installer.nsh Normal file
View File

@@ -0,0 +1,13 @@
!macro customInstall
SetRegView 64
WriteRegStr HKCR "*\shell\rubick" "" "open w&ith rubick search"
WriteRegStr HKCR "*\shell\rubick" "Icon" "$INSTDIR\rubick.exe"
WriteRegStr HKCR "*\shell\rubick\command" "" '"$INSTDIR\rubick.exe" "search" "%1"'
SetRegView 32
WriteRegStr HKCR "*\shell\rubick" "" "open w&ith rubick search"
WriteRegStr HKCR "*\shell\rubick" "Icon" "$INSTDIR\rubick.exe"
WriteRegStr HKCR "*\shell\rubick\command" "" '"$INSTDIR\rubick.exe" "search" "%1"'
!macroend
!macro customUninstall
DeleteRegKey HKCR "*\shell\rubick"
!macroend

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSServices</key>
<array>
<dict>
<key>NSBackgroundColorName</key>
<string>background</string>
<key>NSIconName</key>
<string>NSTouchBarSend</string>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>rubick 打开</string>
</dict>
<key>NSMessage</key>
<string>runWorkflowAsService</string>
<key>NSRequiredContext</key>
<dict>
<key>NSApplicationIdentifier</key>
<string>com.apple.finder</string>
</dict>
<key>NSSendFileTypes</key>
<array>
<string>public.item</string>
</array>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AMApplicationBuild</key>
<string>512</string>
<key>AMApplicationVersion</key>
<string>2.10</string>
<key>AMDocumentVersion</key>
<string>2</string>
<key>actions</key>
<array>
<dict>
<key>action</key>
<dict>
<key>AMAccepts</key>
<dict>
<key>Container</key>
<string>List</string>
<key>Optional</key>
<true/>
<key>Types</key>
<array>
<string>com.apple.cocoa.string</string>
</array>
</dict>
<key>AMActionVersion</key>
<string>2.0.3</string>
<key>AMApplication</key>
<array>
<string>自动操作</string>
</array>
<key>AMParameterProperties</key>
<dict>
<key>COMMAND_STRING</key>
<dict/>
<key>CheckedForUserDefaultShell</key>
<dict/>
<key>inputMethod</key>
<dict/>
<key>shell</key>
<dict/>
<key>source</key>
<dict/>
</dict>
<key>AMProvides</key>
<dict>
<key>Container</key>
<string>List</string>
<key>Types</key>
<array>
<string>com.apple.cocoa.string</string>
</array>
</dict>
<key>ActionBundlePath</key>
<string>/System/Library/Automator/Run Shell Script.action</string>
<key>ActionName</key>
<string>运行Shell脚本</string>
<key>ActionParameters</key>
<dict>
<key>COMMAND_STRING</key>
<string>/Applications/rubick.app/Contents/MacOS/rubick search "$@" &gt; /dev/null 2&gt;&amp;1 &amp;</string>
<key>CheckedForUserDefaultShell</key>
<true/>
<key>inputMethod</key>
<integer>0</integer>
<key>shell</key>
<string>/bin/bash</string>
<key>source</key>
<string></string>
</dict>
<key>BundleIdentifier</key>
<string>com.apple.RunShellScript</string>
<key>CFBundleVersion</key>
<string>2.0.3</string>
<key>CanShowSelectedItemsWhenRun</key>
<false/>
<key>CanShowWhenRun</key>
<true/>
<key>Category</key>
<array>
<string>AMCategoryUtilities</string>
</array>
<key>Class Name</key>
<string>RunShellScriptAction</string>
<key>InputUUID</key>
<string>7E11290E-3E3F-4D65-988B-3E2A8B597EEF</string>
<key>Keywords</key>
<array>
<string>Shell</string>
<string>脚本</string>
<string>命令</string>
<string>运行</string>
<string>Unix</string>
</array>
<key>OutputUUID</key>
<string>475C56B3-F23A-4D39-BB41-4B00F76B096B</string>
<key>UUID</key>
<string>BBBABDAB-D65B-41BF-B1EB-03ACA5AF4714</string>
<key>UnlocalizedApplications</key>
<array>
<string>Automator</string>
</array>
<key>arguments</key>
<dict>
<key>0</key>
<dict>
<key>default value</key>
<integer>0</integer>
<key>name</key>
<string>inputMethod</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>0</string>
</dict>
<key>1</key>
<dict>
<key>default value</key>
<false/>
<key>name</key>
<string>CheckedForUserDefaultShell</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>1</string>
</dict>
<key>2</key>
<dict>
<key>default value</key>
<string></string>
<key>name</key>
<string>source</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>2</string>
</dict>
<key>3</key>
<dict>
<key>default value</key>
<string></string>
<key>name</key>
<string>COMMAND_STRING</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>3</string>
</dict>
<key>4</key>
<dict>
<key>default value</key>
<string>/bin/sh</string>
<key>name</key>
<string>shell</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>4</string>
</dict>
</dict>
<key>conversionLabel</key>
<integer>0</integer>
<key>isViewVisible</key>
<integer>1</integer>
<key>location</key>
<string>309.000000:305.000000</string>
<key>nibPath</key>
<string>/System/Library/Automator/Run Shell Script.action/Contents/Resources/Base.lproj/main.nib</string>
</dict>
<key>isViewVisible</key>
<integer>1</integer>
</dict>
</array>
<key>connectors</key>
<dict/>
<key>workflowMetaData</key>
<dict>
<key>applicationBundleID</key>
<string>com.apple.finder</string>
<key>applicationBundleIDsByPath</key>
<dict>
<key>/System/Library/CoreServices/Finder.app</key>
<string>com.apple.finder</string>
</dict>
<key>applicationPath</key>
<string>/System/Library/CoreServices/Finder.app</string>
<key>applicationPaths</key>
<array>
<string>/System/Library/CoreServices/Finder.app</string>
</array>
<key>inputTypeIdentifier</key>
<string>com.apple.Automator.fileSystemObject</string>
<key>outputTypeIdentifier</key>
<string>com.apple.Automator.nothing</string>
<key>presentationMode</key>
<integer>15</integer>
<key>processesInput</key>
<false/>
<key>serviceApplicationBundleID</key>
<string>com.apple.finder</string>
<key>serviceApplicationPath</key>
<string>/System/Library/CoreServices/Finder.app</string>
<key>serviceInputTypeIdentifier</key>
<string>com.apple.Automator.fileSystemObject</string>
<key>serviceOutputTypeIdentifier</key>
<string>com.apple.Automator.nothing</string>
<key>serviceProcessesInput</key>
<false/>
<key>systemImageName</key>
<string>NSTouchBarSend</string>
<key>useAutomaticInputType</key>
<false/>
<key>workflowTypeIdentifier</key>
<string>com.apple.Automator.servicesMenu</string>
</dict>
</dict>
</plist>

View File

@@ -1,10 +1,20 @@
import path from "path";
import fs from "fs";
import PouchDB from "pouchdb";
import path from 'path';
import fs from 'fs';
import PouchDB from 'pouchdb';
import { DBError, Doc, DocRes } from './types';
import WebDavOP from './webdav';
import { DBError, Doc, DocRes } from "./types";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const replicationStream = require('pouchdb-replication-stream/dist/pouchdb.replication-stream.min.js');
// const load = require('pouchdb-load/dist/pouchdb.load.min.js');
export default class {
PouchDB.plugin(replicationStream.plugin);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const load = require('pouchdb-load');
PouchDB.plugin({ loadIt: load.load });
PouchDB.adapter('writableStream', replicationStream.adapters.writableStream);
export default class DB {
readonly docMaxByteLength;
readonly docAttachmentMaxByteLength;
public dbpath;
@@ -15,7 +25,7 @@ export default class {
this.docMaxByteLength = 2 * 1024 * 1024; // 2M
this.docAttachmentMaxByteLength = 20 * 1024 * 1024; // 20M
this.dbpath = dbPath;
this.defaultDbName = path.join(dbPath, "default");
this.defaultDbName = path.join(dbPath, 'default');
}
init(): void {
@@ -24,11 +34,11 @@ export default class {
}
getDocId(name: string, id: string): string {
return name + "/" + id;
return name + '/' + id;
}
replaceDocId(name: string, id: string): string {
return id.replace(name + "/", "");
return id.replace(name + '/', '');
}
errorInfo(name: string, message: string): DBError {
@@ -38,7 +48,7 @@ export default class {
private checkDocSize(doc: Doc<any>) {
if (Buffer.byteLength(JSON.stringify(doc)) > this.docMaxByteLength) {
return this.errorInfo(
"exception",
'exception',
`doc max size ${this.docMaxByteLength / 1024 / 1024} M`
);
}
@@ -78,15 +88,15 @@ export default class {
async remove(name: string, doc: Doc<any> | string) {
try {
let target;
if ("object" == typeof doc) {
if ('object' == typeof doc) {
target = doc;
if (!target._id || "string" !== typeof target._id) {
return this.errorInfo("exception", "doc _id error");
if (!target._id || 'string' !== typeof target._id) {
return this.errorInfo('exception', 'doc _id error');
}
target._id = this.getDocId(name, target._id);
} else {
if ("string" !== typeof doc) {
return this.errorInfo("exception", "param error");
if ('string' !== typeof doc) {
return this.errorInfo('exception', 'param error');
}
target = await this.pouchDB.get(this.getDocId(name, doc));
}
@@ -94,7 +104,7 @@ export default class {
target._id = result.id = this.replaceDocId(name, result.id);
return result;
} catch (e: any) {
if ("object" === typeof doc) {
if ('object' === typeof doc) {
doc._id = this.replaceDocId(name, doc._id);
}
return this.errorInfo(e.name, e.message);
@@ -107,11 +117,11 @@ export default class {
): Promise<DBError | Array<DocRes>> {
let result;
try {
if (!Array.isArray(docs)) return this.errorInfo("exception", "not array");
if (!Array.isArray(docs)) return this.errorInfo('exception', 'not array');
if (docs.find((e) => !e._id))
return this.errorInfo("exception", "doc not _id field");
return this.errorInfo('exception', 'doc not _id field');
if (new Set(docs.map((e) => e._id)).size !== docs.length)
return this.errorInfo("exception", "_id value exists as");
return this.errorInfo('exception', '_id value exists as');
for (const doc of docs) {
const err = this.checkDocSize(doc);
if (err) return err;
@@ -144,20 +154,20 @@ export default class {
): Promise<DBError | Array<DocRes>> {
const config: any = { include_docs: true };
if (key) {
if ("string" == typeof key) {
if ('string' == typeof key) {
config.startkey = this.getDocId(name, key);
config.endkey = config.startkey + "";
config.endkey = config.startkey + '';
} else {
if (!Array.isArray(key))
return this.errorInfo(
"exception",
"param only key(string) or keys(Array[string])"
'exception',
'param only key(string) or keys(Array[string])'
);
config.keys = key.map((key) => this.getDocId(name, key));
}
} else {
config.startkey = this.getDocId(name, "");
config.endkey = config.startkey + "";
config.startkey = this.getDocId(name, '');
config.endkey = config.startkey + '';
}
const result: Array<any> = [];
try {
@@ -172,4 +182,26 @@ export default class {
}
return result;
}
public async dumpDb(config: {
url: string;
username: string;
password: string;
}): Promise<void> {
const webdavClient = new WebDavOP(config);
webdavClient.createWriteStream(this.pouchDB);
}
public async importDb(config: {
url: string;
username: string;
password: string;
}): Promise<void> {
const webdavClient = new WebDavOP(config);
await this.pouchDB.destroy();
const syncDb = new DB(this.dbpath);
syncDb.init();
this.pouchDB = syncDb.pouchDB;
await webdavClient.createReadStream(this.pouchDB);
}
}

92
src/core/db/webdav.ts Normal file
View File

@@ -0,0 +1,92 @@
import { Notification, app } from 'electron';
import MemoryStream from 'memorystream';
import { createClient } from 'webdav';
import { WebDAVClient } from 'webdav/dist/node/types';
type WebDavOptions = {
username: string;
password: string;
url: string;
};
type DBInstance = {
loadIt: (stream: unknown) => void;
dump: (stream: unknown) => void;
};
export default class WebDav {
public client: WebDAVClient;
private cloudPath = '/rubick/db.txt';
constructor({ username, password, url }: WebDavOptions) {
this.client = createClient(url, {
username,
password,
});
this.client
.exists('/')
.then((result) => {
!result &&
new Notification({
title: '导出失败',
body: 'webdav 连接失败',
}).show();
})
.catch((r) => {
new Notification({
title: '导出失败',
body: 'WebDav连接出错' + r,
}).show();
});
}
async createWriteStream(dbInstance: DBInstance): Promise<void> {
try {
const result = await this.client.exists('/rubick');
if (!result) {
await this.client.createDirectory('/rubick');
}
} catch (e) {
new Notification({
title: '导出失败',
body: 'WebDav目录创建出错:' + e,
}).show();
}
const ws = new MemoryStream();
dbInstance.dump(ws);
ws.pipe(
this.client.createWriteStream(this.cloudPath, {}, () => {
new Notification({
title: '已导出到坚果云',
body: `文件目录为:${this.cloudPath}`,
}).show();
})
);
}
async createReadStream(dbInstance: DBInstance): Promise<void> {
try {
const result = await this.client.exists(this.cloudPath);
if (!result) {
return new Notification({
title: '导入失败',
body: '请确认坚果云上已存在数据',
}).show();
}
const str = await this.client.getFileContents(this.cloudPath, {
format: 'text',
});
await dbInstance.loadIt(str);
new Notification({
title: '导入成功',
body: '数据已导入到 rubick主应用数据需重启后生效',
}).show();
} catch (e) {
new Notification({
title: '导入失败',
body: 'WebDav目录导入出错:' + e,
}).show();
}
}
}

View File

@@ -26,7 +26,6 @@ const runnerInstance = runner();
const detachInstance = detach();
class API extends DBInstance {
public currentPlugin: null | any = null;
init(mainWindow: BrowserWindow) {
// 响应 preload.js 事件
ipcMain.on('msg-trigger', async (event, arg) => {

View File

@@ -5,8 +5,29 @@ const dbInstance = new LocalDb(app.getPath('userData'));
dbInstance.init();
export default class DBInstance {
public currentPlugin: null | any = null;
private DBKEY = 'RUBICK_DB_DEFAULT';
public dbPut({ data }) {
private DB_INFO_KET = 'RUBICK_PLUGIN_INFO';
public async dbPut({ data }) {
// 记录插件有哪些 dbkey用于后续的数据同步
if (this.currentPlugin && this.currentPlugin.name) {
let dbInfo: any = await dbInstance.get(this.DBKEY, this.DB_INFO_KET);
if (!dbInfo) {
dbInfo = { data: [], _id: this.DB_INFO_KET };
}
const item = dbInfo.data.find(
(it) => it.name === this.currentPlugin.name
);
if (item) {
!item.keys.includes(data.data._id) && item.keys.push(data.data._id);
} else {
dbInfo.data.push({
name: this.currentPlugin.name,
keys: [data.data._id],
});
}
dbInstance.put(this.DBKEY, dbInfo);
}
return dbInstance.put(this.DBKEY, data.data);
}
@@ -25,4 +46,12 @@ export default class DBInstance {
public dbAllDocs({ data }) {
return dbInstance.allDocs(this.DBKEY, data.key);
}
public dbDump({ data }) {
return dbInstance.dumpDb(data.target);
}
public dbImport({ data }) {
return dbInstance.importDb(data.target);
}
}

View File

@@ -0,0 +1,84 @@
import path from 'path';
import fs from 'fs-extra';
import os from 'os';
const getSearchFiles = (argv = process.argv, cwd = process.cwd()) => {
const files = argv.slice(2); // 过滤['rubick.exe', 'search']这两个参数,直接获取需要上传的图片路径
let result: any = [];
if (files.length > 0) {
// 如果图片列表不为空
result = files
.map((item) => {
if (path.isAbsolute(item)) {
// 如果是绝对路径
return {
path: item,
};
} else {
const tempPath = path.join(cwd, item); // 如果是相对路径,就拼接
if (fs.existsSync(tempPath)) {
// 判断文件是否存在
return {
path: tempPath,
};
} else {
return null;
}
}
})
.filter((item) => item !== null); // 排除为null的路径
}
return result; // 返回结果
};
const putFileToRubick = (webContents, files) => {
webContents.executeJavaScript(`window.searchFocus(${JSON.stringify(files)})`);
};
const copyFileOutsideOfElectronAsar = function (
sourceInAsarArchive,
destOutsideAsarArchive
) {
if (fs.existsSync(sourceInAsarArchive)) {
// file will be copied
if (fs.statSync(sourceInAsarArchive).isFile()) {
const file = destOutsideAsarArchive;
const dir = path.dirname(file);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(file, fs.readFileSync(sourceInAsarArchive));
}
// dir is browsed
else if (fs.statSync(sourceInAsarArchive).isDirectory()) {
fs.readdirSync(sourceInAsarArchive).forEach(function (fileOrFolderName) {
copyFileOutsideOfElectronAsar(
sourceInAsarArchive + '/' + fileOrFolderName,
destOutsideAsarArchive + '/' + fileOrFolderName
);
});
}
}
};
const macBeforeOpen = () => {
const dest = `${os.homedir}/Library/Services/rubick.workflow`;
if (fs.existsSync(dest)) {
// 判断是否存在
return true;
} else {
// 如果不存在就复制过去
try {
copyFileOutsideOfElectronAsar(
path.join(__static, 'rubick.workflow'),
dest
);
} catch (e) {
console.log(e);
}
}
};
export { getSearchFiles, putFileToRubick, macBeforeOpen };

View File

@@ -9,17 +9,23 @@ export default () => {
let systemPlugins = totalPlugins.filter(
(plugin) => plugin.pluginType === 'system'
);
systemPlugins = systemPlugins.map((plugin) => {
const pluginPath = path.resolve(
PLUGIN_INSTALL_DIR,
'node_modules',
plugin.name
);
return {
...plugin,
indexPath: path.join(pluginPath, './', plugin.entry),
};
});
systemPlugins = systemPlugins
.map((plugin) => {
try {
const pluginPath = path.resolve(
PLUGIN_INSTALL_DIR,
'node_modules',
plugin.name
);
return {
...plugin,
indexPath: path.join(pluginPath, './', plugin.entry),
};
} catch (e) {
return false;
}
})
.filter(Boolean);
const hooks = {
onReady: [],

View File

@@ -13,6 +13,11 @@ import API from './common/api';
import createTray from './common/tray';
import registerHotKey from './common/registerHotKey';
import localConfig from './common/initLocalConfig';
import {
getSearchFiles,
putFileToRubick,
macBeforeOpen,
} from './common/getSearchFiles';
import '../common/utils/localPlugin';
@@ -41,6 +46,7 @@ class App {
beforeReady() {
// 系统托盘
if (commonConst.macOS()) {
macBeforeOpen();
if (commonConst.production() && !app.isInApplicationsFolder()) {
app.moveToApplicationsFolder();
} else {
@@ -83,14 +89,20 @@ class App {
}
onRunning() {
app.on('second-instance', () => {
// 当运行第二个实例时,将会聚焦到myWindow这个窗口
app.on('second-instance', (event, commandLine, workingDirectory) => {
const files = getSearchFiles(commandLine, workingDirectory);
const win = this.windowCreator.getWindow();
// 当运行第二个实例时,将会聚焦到myWindow这个窗口
// 如果有文件列表作为参数,说明是命令行启动
if (win) {
if (win.isMinimized()) {
win.restore();
}
win.focus();
if (files.length > 0) {
win.show();
putFileToRubick(win.webContents, files);
}
}
});
app.on('activate', () => {

View File

@@ -59,7 +59,7 @@ import { LoadingOutlined, MoreOutlined } from '@ant-design/icons-vue';
const remote = window.require('@electron/remote');
import localConfig from '../confOp';
const { Menu } = remote;
const { Menu, app } = remote;
const config: any = ref(localConfig.getConfig());
@@ -232,9 +232,14 @@ const changeHideOnBlur = () => {
const getIcon = () => {
if (props.clipboardFile[0].dataUrl) return props.clipboardFile[0].dataUrl;
return props.clipboardFile[0].isFile
? require('../assets/file.png')
: require('../assets/folder.png');
try {
return ipcRenderer.sendSync('msg-trigger', {
type: 'getFileIcon',
data: { path: props.clipboardFile[0].path },
});
} catch (e) {
return require('../assets/file.png');
}
};
const newWindow = () => {

View File

@@ -8,13 +8,13 @@ import { ref } from 'vue';
export default ({ currentPlugin, optionsRef, openPlugin, setOptionsRef }) => {
const clipboardFile: any = ref([]);
const searchFocus = () => {
const searchFocus = (files) => {
const config: any = localConfig.getConfig();
// 未开启自动粘贴
if (!config.perf.common.autoPast) return;
if (currentPlugin.value.name) return;
const fileList = getCopyFiles();
const fileList = files || getCopyFiles();
// 拷贝的是文件
if (fileList) {
window.setSubInputValue({ value: '' });
@@ -43,78 +43,79 @@ export default ({ currentPlugin, optionsRef, openPlugin, setOptionsRef }) => {
}
// 再正则插件
localPlugins.forEach((plugin) => {
const feature = plugin.features;
// 系统插件无 features 的情况,不需要再搜索
if (!feature) return;
feature.forEach((fe) => {
const ext = path.extname(fileList[0].path);
fe.cmds.forEach((cmd) => {
const regImg = /\.(png|jpg|gif|jpeg|webp)$/;
if (
cmd.type === 'img' &&
regImg.test(ext) &&
fileList.length === 1
) {
const option = {
name: cmd.label,
value: 'plugin',
icon: plugin.logo,
desc: fe.explain,
type: plugin.pluginType,
click: () => {
pluginClickEvent({
plugin,
fe,
cmd,
ext: {
code: fe.code,
type: cmd.type || 'text',
payload: nativeImage
.createFromPath(fileList[0].path)
.toDataURL(),
},
openPlugin,
option,
});
clearClipboardFile();
},
};
options.push(option);
}
// 如果是文件,且符合文件正则类型
if (
fileList.length > 1 ||
(cmd.type === 'file' && new RegExp(cmd.match).test(ext))
) {
const option = {
name: cmd,
value: 'plugin',
icon: plugin.logo,
desc: fe.explain,
type: plugin.pluginType,
click: () => {
pluginClickEvent({
plugin,
fe,
cmd,
option,
ext: {
code: fe.code,
type: cmd.type || 'text',
payload: fileList,
},
openPlugin,
});
clearClipboardFile();
},
};
options.push(option);
}
if (fileList.length === 1) {
localPlugins.forEach((plugin) => {
const feature = plugin.features;
// 系统插件无 features 的情况,不需要再搜索
if (!feature) return;
feature.forEach((fe) => {
const ext = path.extname(fileList[0].path);
fe.cmds.forEach((cmd) => {
const regImg = /\.(png|jpg|gif|jpeg|webp)$/;
if (
cmd.type === 'img' &&
regImg.test(ext) &&
fileList.length === 1
) {
const option = {
name: cmd.label,
value: 'plugin',
icon: plugin.logo,
desc: fe.explain,
type: plugin.pluginType,
click: () => {
pluginClickEvent({
plugin,
fe,
cmd,
ext: {
code: fe.code,
type: cmd.type || 'text',
payload: nativeImage
.createFromPath(fileList[0].path)
.toDataURL(),
},
openPlugin,
option,
});
clearClipboardFile();
},
};
options.push(option);
}
// 如果是文件,且符合文件正则类型
if (
fileList.length > 1 ||
(cmd.type === 'file' && new RegExp(cmd.match).test(ext))
) {
const option = {
name: cmd,
value: 'plugin',
icon: plugin.logo,
desc: fe.explain,
type: plugin.pluginType,
click: () => {
pluginClickEvent({
plugin,
fe,
cmd,
option,
ext: {
code: fe.code,
type: cmd.type || 'text',
payload: fileList,
},
openPlugin,
});
clearClipboardFile();
},
};
options.push(option);
}
});
});
});
});
}
setOptionsRef(options);
clipboard.clear();
return;

View File

@@ -40,7 +40,7 @@ const createPluginManager = (): any => {
},
});
if (result && result.value) {
appList.value = [...appList.value, ...result.value];
appList.value.push(...result.value);
}
};

View File

@@ -156,6 +156,8 @@ const optionsManager = ({
setOptionsRef,
});
window.searchFocus = searchFocus;
return {
options: optionsRef,
searchFocus,

View File

@@ -26,4 +26,5 @@ interface Window {
setCurrentPlugin: (plugin: any) => void;
pluginLoaded: () => void;
getMainInputInfo: () => any;
searchFocus: (args: any) => any;
}

View File

@@ -32,7 +32,7 @@ module.exports = {
],
// Use this to change the entry point of your app's render process. default src/[main|index].[js|ts]
builderOptions: {
productName: 'rubick2',
productName: 'rubick',
appId: 'com.muwoo.rubick',
compression: 'maximum',
directories: {
@@ -67,7 +67,13 @@ module.exports = {
},
mac: {
icon: 'public/icons/icon.icns',
target: ['dmg', 'zip'],
target: [
{
target: 'dmg',
arch: ['x64', 'arm64'],
},
],
artifactName: 'rubick-${version}-${arch}.dmg',
gatekeeperAssess: false,
entitlementsInherit: './release/entitlements.mac.plist',
entitlements: './release/entitlements.mac.plist',
@@ -79,11 +85,19 @@ module.exports = {
},
win: {
icon: 'public/icons/icon.ico',
target: 'nsis',
artifactName: 'rubick-Setup-${version}-${arch}.exe',
target: [
{
target: 'nsis',
arch: ['x64', 'ia32'],
},
],
},
nsis: {
shortcutName: 'rubick',
oneClick: false,
allowToChangeInstallationDirectory: true,
include: 'public/installer.nsh',
},
linux: {
icon: 'public/icons/',

752
yarn.lock

File diff suppressed because it is too large Load Diff