Sub-Store 1.2 支持生成远程配置

现在允许用户生成配置并上传到Gist。
This commit is contained in:
Peng-YM 2020-12-08 20:19:46 +08:00
parent 29abac4619
commit bb8bac760e
10 changed files with 403 additions and 36 deletions

View File

@ -581,14 +581,18 @@ function service() {
async function deleteArtifact(req, res) {
const name = req.params.name;
$.info(`正在删除Artifact${name}`);
try {
const allArtifacts = $.read(ARTIFACTS_KEY);
if (!allArtifacts[name]) throw new Error(`远程配置:${name}不存在!`);
const artifact = allArtifacts[name];
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) {
// delete gist
await syncArtifact({
filename: name,
content: ""
});
}
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);

File diff suppressed because one or more lines are too long

5
web/package-lock.json generated
View File

@ -10734,6 +10734,11 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
"timeago.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
"integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w=="
},
"timers-browserify": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz",

View File

@ -14,6 +14,7 @@
"chartist": "^0.11.4",
"core-js": "^3.6.5",
"material-design-icons-iconfont": "^5.0.1",
"timeago.js": "^4.0.2",
"v-clipboard": "^2.2.3",
"vee-validate": "^3.4.5",
"vue": "^2.6.12",

View File

@ -41,6 +41,9 @@ function initStore(store) {
store.dispatch("FETCH_COLLECTIONS").catch(() => {
showError(`无法拉取组合订阅列表!`);
});
store.dispatch("FETCH_ARTIFACTS").catch(() => {
showError(`无法拉取配置列表!`);
});
store.dispatch("FETCH_ENV").catch(() => {
showError(`无法获取当前运行环境!`);
});

View File

@ -8,6 +8,11 @@
>
<v-btn :to="{path: '/'}" value="subscription">
<span>订阅</span>
<v-icon>flight_takeoff</v-icon>
</v-btn>
<v-btn :to="{path: '/cloud'}" value="artifacts">
<span>同步</span>
<v-icon>mdi-cloud</v-icon>
</v-btn>

View File

@ -6,6 +6,7 @@ import Subscription from "@/views/Subscription";
import Dashboard from "@/views/Dashboard";
import User from "@/views/User";
import SubEditor from "@/views/SubEditor";
import Cloud from "@/views/Cloud";
Vue.use(Router);
@ -24,6 +25,12 @@ const router = new Router({
component: Dashboard,
meta: {title: "首页"}
},
{
path: "/cloud",
name: "artifact",
component: Cloud,
meta: {title: "同步"}
},
{
path: "/user",
name: "user",

View File

@ -15,6 +15,7 @@ const store = new Vuex.Store({
subscriptions: {},
collections: {},
artifacts: {},
env: {},
settings: {}
@ -57,6 +58,12 @@ const store = new Vuex.Store({
state.collections = data;
});
},
async FETCH_ARTIFACTS({state}) {
return axios.get("/artifacts").then(resp => {
const {data} = resp.data;
state.artifacts = data;
});
},
// fetch env
async FETCH_ENV({state}) {
return axios.get("/utils/env").then(resp => {

299
web/src/views/Cloud.vue Normal file
View File

@ -0,0 +1,299 @@
<template>
<v-container>
<v-card>
<v-card-title>
配置
<v-spacer></v-spacer>
<!-- <v-btn icon>-->
<!-- <v-icon>mdi-cloud-circle</v-icon>-->
<!-- </v-btn>-->
<!-- <v-btn icon>-->
<!-- <v-icon>mdi-refresh-circle</v-icon>-->
<!-- </v-btn>-->
<v-dialog max-width="400px" v-model="addArtifactDialog">
<template #activator="{on}">
<v-btn icon v-on="on">
<v-icon color="primary">mdi-plus-circle</v-icon>
</v-btn>
</template>
<v-card class="pl-4 pr-4 pb-4 pt-4">
<v-subheader>
<v-icon left>mdi-plus-circle</v-icon>
<h3>添加同步配置</h3>
</v-subheader>
<v-divider></v-divider>
<v-form class="pt-4 pl-4 pr-4 pb-0" v-model="formValid">
<v-text-field
v-model="newArtifact.name"
label="配置名称"
placeholder="填入生成配置名称名称需唯一如Clash.yaml。"
:rules="validations.nameRules"
clearable
clear-icon="clear"
/>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field
label="类型"
v-on="on"
:rules="validations.required"
:value="getType(newArtifact.type)"
/>
</template>
<v-list dense>
<v-list-item @click="setArtifactType('subscription')">
<v-list-item-icon>
<v-icon>mdi-link</v-icon>
</v-list-item-icon>
<v-list-item-title>订阅</v-list-item-title>
</v-list-item>
<v-list-item @click="setArtifactType('collection')">
<v-list-item-icon>
<v-icon>list</v-icon>
</v-list-item-icon>
<v-list-item-title>组合订阅</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field
v-model="newArtifact.source"
label="来源"
:rules="validations.required"
:placeholder="`填入${getType(newArtifact.type) || '来源'}的名称。`"
v-on="on"
/>
</template>
<v-list dense>
<v-list-item
v-for="(name, idx) in getSources(newArtifact.type)"
@click="newArtifact.source = name"
:key="idx"
>
<v-list-item-title>{{ name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field
label="目标"
v-on="on"
:rules="validations.required"
:value="newArtifact.platform"
/>
</template>
<v-list dense>
<v-list-item
v-for="platform in ['Surge', 'Loon', 'QX', 'Clash']"
:key="platform"
@click="newArtifact.platform = platform"
>
<v-list-item-avatar>
<v-img :src="getIcon(platform)"></v-img>
</v-list-item-avatar>
<v-list-item-title>{{ platform }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-form>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text small :disabled="!formValid" @click="createArtifact()">
确认
</v-btn>
<v-btn text small @click="clear()">
取消
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card-title>
<template v-for="(artifact, idx) in artifacts">
<v-list-item three-line dense :key="artifact.name">
<v-list-item-avatar>
<v-img :src="getIcon(artifact.platform)"/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ artifact.name }}
</v-list-item-title>
<v-chip-group>
<v-chip label>
<v-icon left>info</v-icon>
{{ getType(artifact.type) }}
</v-chip>
<v-chip label>
<v-icon left>mdi-link</v-icon>
{{ artifact.source }}
</v-chip>
</v-chip-group>
<v-list-item-subtitle>更新于{{ getUpdatedTime(artifact.updated) }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-menu bottom left>
<template #activator="{ on }">
<v-btn icon v-on="on">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item @click="copy(artifact.url)">
<v-list-item-title>复制</v-list-item-title>
</v-list-item>
<v-list-item @click="preview(artifact.name)">
<v-list-item-title>预览</v-list-item-title>
</v-list-item>
<v-list-item @click="sync(artifact.name)">
<v-list-item-title>同步</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteArtifact(idx, artifact.name)">
<v-list-item-title>删除</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-list-item-action>
</v-list-item>
</template>
</v-card>
</v-container>
</template>
<script>
import {axios} from '@/utils';
import {BACKEND_BASE} from "@/config";
import {format} from 'timeago.js';
export default {
name: "Cloud",
data() {
return {
addArtifactDialog: false,
newArtifact: {
name: "",
type: "subscription",
source: "",
platform: "",
},
formValid: false,
validations: {
nameRules: [
v => !!v || "订阅名称不能为空!",
v => /^[\w-_.]*$/.test(v) || "订阅名称只能包含英文字符、横杠、点和下划线!"
],
required: [
v => !!v || "不能为空!"
]
}
}
},
created() {
axios.get("/artifacts").then(resp => {
const {data} = resp.data;
this.artifacts = Object.keys(data).map(k => data[k]);
})
},
computed: {
artifacts() {
const items = this.$store.state.artifacts;
return Object.keys(items).map(k => items[k]);
}
},
methods: {
getIcon(platform) {
const ICONS = {
"Clash": "https://github.com/Dreamacro/clash/raw/master/docs/logo.png",
"QX": "https://raw.githubusercontent.com/Orz-3/task/master/quantumultx.png",
"Surge": "https://raw.githubusercontent.com/Orz-3/task/master/surge.png",
"Loon": "https://raw.githubusercontent.com/Orz-3/task/master/loon.png"
}
return ICONS[platform];
},
getType(type) {
const DESCRIPTIONS = {
"subscription": "订阅",
"collection": "组合订阅"
}
return DESCRIPTIONS[type];
},
getUpdatedTime(time) {
if (!time) {
return "从未更新";
} else {
return format(time, "zh_CN");
}
},
async createArtifact() {
try {
await axios.post("/artifacts", this.newArtifact);
await this.$store.dispatch("FETCH_ARTIFACTS");
this.clear();
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `创建配置失败!${err}`);
}
},
async deleteArtifact(idx, name) {
try {
await axios.delete(`/artifact/${name}`);
await this.$store.dispatch("FETCH_ARTIFACTS");
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `删除配置失败!${err}`);
}
},
clear() {
this.newArtifact = {
name: "",
type: "subscription",
source: "",
platform: ""
}
this.addArtifactDialog = false;
},
copy(url) {
this.$clipboard(url);
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制配置链接");
},
preview(name) {
window.open(`${BACKEND_BASE}/api/artifact/${name}?action=preview`);
},
async sync(name) {
try {
await axios.get(`/artifact/${name}?action=sync`);
await this.$store.dispatch("FETCH_ARTIFACTS");
this.$store.commit("SET_SUCCESS_MESSAGE", `同步配置成功!`);
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `同步配置失败!${err}`);
}
},
setArtifactType(type) {
this.newArtifactType = type;
this.newArtifact.source = "";
},
getSources(type) {
let data;
switch (type) {
case "subscription":
data = this.$store.state.subscriptions;
break;
case "collection":
data = this.$store.state.collections;
}
return Object.keys(data);
}
}
}
</script>

View File

@ -1,13 +1,20 @@
<template>
<v-card
class="mb-4 ml-4 mr-4 mt-4"
>
<v-container>
<v-card>
<v-card-title>
云同步
GitHub 配置
<v-spacer></v-spacer>
<v-icon small>cloud</v-icon>
<v-icon small>settings</v-icon>
</v-card-title>
<v-card-text>
<v-text-field
label="GitHub 用户名"
hint="填入GitHub用户名"
v-model="settings.githubUser"
clearable clear-icon="clear"
>
</v-text-field>
<v-text-field
label="GitHub Token"
hint="填入GitHub Token"
@ -18,32 +25,53 @@
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn label @click="sync('upload')">上传</v-btn>
<v-btn label @click="sync('download')">下载</v-btn>
<v-btn small text @click="save()" color="primary">保存</v-btn>
</v-card-actions>
<v-divider/>
</v-card>
<v-card>
<v-card-title>
Gist 数据同步
<v-spacer></v-spacer>
<v-icon small>mdi-cloud</v-icon>
</v-card-title>
<v-card-text>
最近同步于{{getSyncTime()}}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn small text @click="sync('upload')">上传</v-btn>
<v-btn small text @click="sync('download')">恢复</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script>
import {axios, showError} from "@/utils";
import {format} from "timeago.js";
export default {
data() {
return {
settings: {
gistToken: ""
gistToken: "",
githubUser: "",
syncTime: ""
}
}
},
created() {
axios.get(`/settings`).then(resp => {
this.settings.gistToken = resp.data.gistToken;
this.settings.githubUser = resp.data.githubUser;
});
},
methods: {
save() {
axios.patch(`/settings`, this.settings);
this.$store.commit("SET_SUCCESS_MESSAGE", `保存成功!`);
},
sync(action) {
@ -51,13 +79,13 @@ export default {
this.$store.commit("SET_ERROR_MESSAGE", "未设置GitHub Token");
return;
}
this.save();
axios.get(`/utils/backup?action=${action}`).then(resp => {
if (resp.data.status === 'success') {
this.$store.commit("SET_SUCCESS_MESSAGE", `${action === 'upload' ? "备份" : "还原"}成功!`);
this.settings.syncTime = new Date().getTime();
axios.patch(`/settings`, this.settings);
this.updateStore(this.$store);
}
else
} else
this.$store.commit("SET_ERROR_MESSAGE", `备份失败!${resp.data.message}`);
});
},
@ -69,6 +97,14 @@ export default {
store.dispatch("FETCH_COLLECTIONS").catch(() => {
showError(`无法拉取组合订阅列表!`);
});
},
getSyncTime() {
if (this.settings.syncTime) {
return format(this.settings.syncTime, "zh_CN");
} else {
return "从未同步";
}
}
}
}