mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-09-12 17:53:13 +08:00
Sub-Store 1.2 支持生成远程配置
现在允许用户生成配置并上传到Gist。
This commit is contained in:
parent
29abac4619
commit
bb8bac760e
@ -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}不存在!`);
|
||||
// delete gist
|
||||
await syncArtifact({
|
||||
filename: name,
|
||||
content: ""
|
||||
});
|
||||
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);
|
||||
|
4
backend/sub-store.min.js
vendored
4
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
5
web/package-lock.json
generated
5
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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(`无法获取当前运行环境!`);
|
||||
});
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
299
web/src/views/Cloud.vue
Normal 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>
|
@ -1,49 +1,77 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="mb-4 ml-4 mr-4 mt-4"
|
||||
>
|
||||
<v-card-title>
|
||||
云同步
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon small>cloud</v-icon>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
label="GitHub Token"
|
||||
hint="填入GitHub Token"
|
||||
v-model="settings.gistToken"
|
||||
clearable clear-icon="clear"
|
||||
/>
|
||||
</v-card-text>
|
||||
<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-card-actions>
|
||||
<v-divider/>
|
||||
</v-card>
|
||||
<v-container>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
GitHub 配置
|
||||
<v-spacer></v-spacer>
|
||||
<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"
|
||||
v-model="settings.gistToken"
|
||||
clearable clear-icon="clear"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<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 "从未同步";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user