feat: add preview specific platform feature (#131)

This commit is contained in:
Jacob Lee 2022-06-25 13:17:12 +08:00 committed by GitHub
parent 013b2173fd
commit 9202437f05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 14392 additions and 245 deletions

14065
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

2
web/.gitignore vendored
View File

@ -1,7 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dist /dist
yarn.lock
# local env files # local env files
.env.local .env.local

3
web/.prettierignore Normal file
View File

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

47
web/.prettierrc.yaml Normal file
View File

@ -0,0 +1,47 @@
printWidth : 80 # 显示宽度
tabWidth : 2 # tab 宽度
useTabs : false # 使用 tab 而不是空格
semi : false # 使用分号
singleQuote : true # 使用单引号
jsxSingleQuote : false # 在 JSX 中使用单引号而不是双引号
bracketSpacing : true # 在对象花括号内打印空格 true { foo: bar } false {foo: bar}
arrowParens : "avoid" # 箭头函数只有一个参数的时候的周围的括号 "always" - (x) => x "avoid" - x => x
embeddedLanguageFormatting : "auto" # "auto" - 嵌入代码如果 Prettier 可以识别则格式化它 "off" - 永远不要自动格式化
bracketSameLine : false # 多行属性的 HTMLHTML、JSX、Vue、Angular标签的 ">" 放在最后一行的末尾,而不是单独在下一行(不适用于自闭合元素)
#htmlWhitespaceSensitivity : "strict"
vueIndentScriptAndStyle : true # 在 Vue 文件中缩进 <script> 和 <style> 标签
insertPragma : false # 是否插入一个特殊 @format 标记指定文件已使用 Prettier 格式化
# 行尾风格
# "lf" 仅换行 ( \n),常见于 Linux 和 macOS 以及 git repos 内部
# "crlf" - 回车 + 换行字符 ( \r\n),常见于 Windows
# "cr" - 仅回车字符 ( \r),很少使用
# "auto" - 保持现有的行尾(一个文件中的混合值通过查看第一行之后使用的内容进行标准化)
endOfLine : "lf"
# 需要提供注释才允许格式化
# /**
# * @prettier 或 @format
# */
requirePragma : false
# 对象属性的引号风格
# "as-needed" 仅在需要时在对象属性周围添加引号
# "consistent" 如果对象中至少一个属性需要引号,则所有属性都使用引号
# "preserve" 尊重对象属性中的引号
quoteProps : "consistent"
# 在多行逗号分隔的句法结构中尽可能打印尾随逗号
# "es5" 在 ES5 中有效的尾随逗号对象、数组等TypeScript 中的类型参数中没有尾随逗号
# "none" 没有尾随逗号。
# "all" 尽可能使用尾随逗号(包括函数参数和调用)。要运行,以这种方式格式化的 JavaScript 代码需要一个支持 ES2017Node.js 8+ 或现代浏览器)或下级编译的引擎。这还可以在 TypeScript 中的类型参数中启用尾随逗号(自 2018 年 1 月发布的 TypeScript 2.7 起支持)
trailingComma : "es5"
# 例外配置覆盖
overrides :
- files :
- "*.ts"
- "*.tsx"
options :
semi : true
arrowParens : "always"

View File

@ -1,70 +1,64 @@
<template> <template>
<div class="float-menu-switch-wrapper" ref = "floatMenuSwitch"> <div class = "float-menu-switch-wrapper" ref = "floatMenuSwitch">
<v-speed-dial <v-speed-dial v-model = "fab" :direction = "direction" :transition = "transition"
v-model="fab" >
:direction="direction" <template v-slot:activator>
:transition="transition" <v-btn v-model = "fab" color = "primary" fab>
> <v-icon v-if = "fab"> mdi-close</v-icon>
<template v-slot:activator> <v-icon v-else> mdi-gesture-double-tap</v-icon>
<v-btn v-model="fab" color="primary" fab> </v-btn>
<v-icon v-if="fab"> mdi-close</v-icon> </template>
<v-icon v-else> mdi-gesture-double-tap</v-icon> <slot></slot>
</v-btn> </v-speed-dial>
</template>
<slot></slot>
</v-speed-dial>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: "FloatMenu", name : "FloatMenu",
data() { data (){
return { return {
direction: "top", direction : "top",
fab: false, fab : false,
fling: false, fling : false,
hover: false, hover : false,
tabs: null, tabs : null,
transition: "scale-transition", transition : "scale-transition",
}; };
},
updated (){
const floatMenuSwitch = this.$refs.floatMenuSwitch;
console.log(floatMenuSwitch);
floatMenuSwitch.style.bottom = 2*this.bottomNavBarHeight + "px";
},
computed : {
bottomNavBarHeight (){
return this.$store.state.bottomNavBarHeight;
}, },
}, updated (){
}; const floatMenuSwitch = this.$refs.floatMenuSwitch;
floatMenuSwitch.style.bottom = 2 * this.bottomNavBarHeight + "px";
},
computed : {
bottomNavBarHeight (){
return this.$store.state.bottomNavBarHeight;
},
},
};
</script> </script>
<style lang="scss" scoped> <style lang = "scss" scoped>
.float-menu-switch-wrapper { .float-menu-switch-wrapper {
position : fixed; position : fixed;
right: 16px;; right : 16px;;
z-index : 99; z-index : 99;
.v-speed-dial > button.v-btn.v-btn--round { .v-speed-dial > button.v-btn.v-btn--round {
margin-right : 0; margin-right : 0;
width : 40px; width : 40px;
height : 40px; height : 40px;
} }
::v-deep .v-speed-dial__list button.theme--light.v-btn { ::v-deep .v-speed-dial__list button.theme--light.v-btn {
margin-right : 0; margin-right : 0;
} }
} }
/* This is for documentation purposes and will not be needed in your application */ /* This is for documentation purposes and will not be needed in your application */
#create .v-speed-dial { #create .v-speed-dial {
position: absolute; position : absolute;
} }
#create .v-btn--floating { #create .v-btn--floating {
position: relative; position : relative;
} }
</style> </style>

View File

@ -1,39 +1,74 @@
<template> <template>
<v-container fluid> <v-container fluid>
<v-dialog v-model = "showPreviewDialog" scrollable>
<v-card>
<v-card-title>预览转换结果</v-card-title>
<v-divider></v-divider>
<v-list flat>
<v-list-item v-for = "platform in platformList" :key = "platform.name"
@click = "previewSpecificPlatform(platform.path)"
>
<v-list-item-avatar>
<v-img :class = "getIconClass('#invert')" :src = "platform.icon"
/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text = "platform.name"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click = "showPreviewDialog = false">取消</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-card> <v-card>
<v-card-title> <v-card-title>
<v-icon left>local_airport</v-icon> <v-icon left>local_airport</v-icon>
单个订阅 单个订阅
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="createSub"> <v-btn icon @click = "createSub">
<v-icon color="primary">mdi-plus-circle</v-icon> <v-icon color = "primary">mdi-plus-circle</v-icon>
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-list dense> <v-list dense>
<v-list-item v-for="sub in subscriptions" :key="sub.name" @click="preview(sub)"> <v-list-item v-for = "sub in subscriptions" :key = "sub.name"
@click = "preview(sub)"
>
<v-list-item-avatar> <v-list-item-avatar>
<v-icon v-if="!sub.icon" color="teal darken-1">mdi-cloud</v-icon> <v-icon v-if = "!sub.icon" color = "teal darken-1">mdi-cloud
<v-img v-else :class="getIconClass(sub.icon)" :src="sub.icon" /> </v-icon>
<v-img v-else :class = "getIconClass(sub.icon)"
:src = "sub.icon"
/>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title class="font-weight-medium" v-text="sub['display-name'] || sub.name"> <v-list-item-title class = "font-weight-medium"
</v-list-item-title> v-text = "sub['display-name'] || sub.name"
<v-list-item-title v-text="sub.url"></v-list-item-title> ></v-list-item-title>
<v-list-item-title v-text = "sub.url"></v-list-item-title>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-menu bottom left> <v-menu bottom left>
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator = "{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" icon> <v-btn v-bind = "attrs" v-on = "on" icon>
<v-icon>mdi-dots-vertical</v-icon> <v-icon>mdi-dots-vertical</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-item v-for="(menuItem, i) in editMenu" :key="i" <v-list-item v-for = "(menuItem, i) in editMenu" :key = "i"
@click="subscriptionMenu(menuItem.action, sub)"> @click = "subscriptionMenu(menuItem.action, sub)"
<v-list-item-content>{{ menuItem.title }}</v-list-item-content> >
<v-list-item-content>{{
menuItem.title
}}
</v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
@ -48,39 +83,52 @@
<v-icon left>work_outline</v-icon> <v-icon left>work_outline</v-icon>
组合订阅 组合订阅
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn icon @click="createCol"> <v-btn icon @click = "createCol">
<v-icon color="primary">mdi-plus-circle</v-icon> <v-icon color = "primary">mdi-plus-circle</v-icon>
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-list dense> <v-list dense>
<v-list-item v-for="collection in collections" :key="collection.name" dense <v-list-item v-for = "collection in collections"
@click="preview(collection, type='collection')"> :key = "collection.name" dense
@click = "preview(collection, type='collection')"
>
<v-list-item-avatar> <v-list-item-avatar>
<v-icon v-if="!collection.icon" color="teal darken-1">mdi-cloud</v-icon> <v-icon v-if = "!collection.icon" color = "teal darken-1">
<v-img v-else :class="getIconClass(collection.icon)" :src="collection.icon" /> mdi-cloud
</v-icon>
<v-img v-else :class = "getIconClass(collection.icon)"
:src = "collection.icon"
/>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title class="font-weight-medium" v-text="collection['display-name'] || collection.name"> <v-list-item-title class = "font-weight-medium"
</v-list-item-title> v-text = "collection['display-name'] || collection.name"
></v-list-item-title>
<v-chip-group column> <v-chip-group column>
<v-chip v-for="subs in collection.subsInfo" :key="subs.name" class="ma-2 ml-0 mr-1 pa-2" label small> <v-chip v-for = "subs in collection.subsInfo" :key = "subs.name"
{{ subs['display-name'] || subs.name}} class = "ma-2 ml-0 mr-1 pa-2" label small
>
{{ subs['display-name'] || subs.name }}
</v-chip> </v-chip>
</v-chip-group> </v-chip-group>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-menu bottom left> <v-menu bottom left>
<template v-slot:activator="{ on, attrs }"> <template v-slot:activator = "{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" icon> <v-btn v-bind = "attrs" v-on = "on" icon>
<v-icon>mdi-dots-vertical</v-icon> <v-icon>mdi-dots-vertical</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-item v-for="(menuItem, i) in editMenu" :key="i" <v-list-item v-for = "(menuItem, i) in editMenu" :key = "i"
@click="collectionMenu(menuItem.action, collection)"> @click = "collectionMenu(menuItem.action, collection)"
<v-list-item-content>{{ menuItem.title }}</v-list-item-content> >
<v-list-item-content>{{
menuItem.title
}}
</v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
@ -89,9 +137,11 @@
</v-list> </v-list>
</v-card-text> </v-card-text>
</v-card> </v-card>
<v-dialog v-model="showProxyList" fullscreen hide-overlay scrollable transition="dialog-bottom-transition"> <v-dialog v-model = "showProxyList" fullscreen hide-overlay scrollable
transition = "dialog-bottom-transition"
>
<v-card fluid> <v-card fluid>
<v-toolbar class="flex-grow-0"> <v-toolbar class = "flex-grow-0">
<v-icon>mdi-dns</v-icon> <v-icon>mdi-dns</v-icon>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-toolbar-title> <v-toolbar-title>
@ -99,29 +149,33 @@
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-toolbar-items> <v-toolbar-items>
<v-btn icon @click="showProxyList = false"> <v-btn icon @click = "showProxyList = false">
<v-icon>mdi-close</v-icon> <v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
</v-toolbar-items> </v-toolbar-items>
<template v-slot:extension> <template v-slot:extension>
<v-tabs v-model="tab" centered grow> <v-tabs v-model = "tab" centered grow>
<v-tabs-slider color="primary" /> <v-tabs-slider color = "primary" />
<v-tab key="raw"> <v-tab key = "raw">
<h4>原始节点</h4> <h4>原始节点</h4>
</v-tab> </v-tab>
<v-tab key="processed"> <v-tab key = "processed">
<h4>生成节点</h4> <h4>生成节点</h4>
</v-tab> </v-tab>
</v-tabs> </v-tabs>
</template> </template>
</v-toolbar> </v-toolbar>
<v-card-text> <v-card-text>
<v-tabs-items v-model="tab"> <v-tabs-items v-model = "tab">
<v-tab-item key="raw"> <v-tab-item key = "raw">
<proxy-list :key="url + 'raw'" ref="proxyList" :raw="true" :sub="sub" :url="url"></proxy-list> <proxy-list :key = "url + 'raw'" ref = "proxyList" :raw = "true"
:sub = "sub" :url = "url"
></proxy-list>
</v-tab-item> </v-tab-item>
<v-tab-item key="processed"> <v-tab-item key = "processed">
<proxy-list :key="url" ref="proxyList" :sub="sub" :url="url"></proxy-list> <proxy-list :key = "url" ref = "proxyList" :sub = "sub"
:url = "url"
></proxy-list>
</v-tab-item> </v-tab-item>
</v-tabs-items> </v-tabs-items>
</v-card-text> </v-card-text>
@ -131,129 +185,187 @@
</template> </template>
<script> <script>
import ProxyList from "@/components/ProxyList"; import ProxyList from '@/components/ProxyList'
import {BACKEND_BASE} from "@/config"; import { BACKEND_BASE } from '@/config'
export default { export default {
components: {ProxyList}, components : { ProxyList },
data: () => { data : () => {
return { return {
opened: false, opened : false,
showProxyList: false, showProxyList : false,
url: "", showPreviewDialog : false,
sub: [], previewSubName : '',
tab: 1, isCollectionPreview : false,
editMenu: [ url : '',
{ sub : [],
title: "链接", tab : 1,
action: "COPY" platformList : [
}, {
{ name : 'Clash',
title: "编辑", path : 'Clash',
action: "EDIT" icon : 'https://raw.githubusercontent.com/58xinian/icon/master/clash_mini.png',
}, },
{ {
title: "删除", name : 'Quantumult X',
action: "DELETE" path : 'QX',
} icon : 'https://raw.githubusercontent.com/Orz-3/mini/none/quanX.png',
] },
} {
}, name : 'Surge',
path : 'Surge',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/none/surge.png',
},
{
name : 'Loon',
path : 'Loon',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/none/loon.png',
},
computed: { {
subscriptionBaseURL() { name : 'Stash',
return BACKEND_BASE; path : 'Stash',
icon : 'https://raw.githubusercontent.com/Orz-3/mini/master/Alpha/stash.png',
}
],
editMenu : [
{
title : '链接',
action : 'COPY'
},
{
title : '编辑',
action : 'EDIT'
},
{
title : '预览',
action : 'PREVIEW'
},
{
title : '删除',
action : 'DELETE'
}
]
}
}, },
subscriptions: {
get() { computed : {
const subs = this.$store.state.subscriptions; subscriptionBaseURL (){
return Object.keys(subs).map(k => subs[k]); return BACKEND_BASE
}, },
set() { subscriptions : {
get (){
const subs = this.$store.state.subscriptions
return Object.keys(subs).map(k => subs[k])
},
set (){
} }
},
collections (){
const cols = this.$store.state.collections
const collections = Object.keys(cols).map(k => cols[k])
const subscriptions = this.$store.state.subscriptions
collections.map(item => {
item.subsInfo = []
item.subscriptions.map(sub => item.subsInfo.push(subscriptions[sub]))
})
return collections
},
}, },
collections() {
const cols = this.$store.state.collections;
const collections = Object.keys(cols).map(k => cols[k]);
const subscriptions = this.$store.state.subscriptions;
collections.map(item => {
item.subsInfo = []
item.subscriptions.map(sub => item.subsInfo.push(subscriptions[sub]))
})
return collections
},
},
methods: { methods : {
subscriptionMenu(action, sub) { previewSpecificPlatform (path){
console.log(`${action} --> ${sub.name}`); window.open(`${this.subscriptionBaseURL}/download/${this.isCollectionPreview ? 'collection/' : ''}${this.previewSubName}?target=${path}`)
switch (action) { this.showPreviewDialog = false
case 'COPY': },
this.$clipboard(`${this.subscriptionBaseURL}/download/${encodeURIComponent(sub.name)}`); subscriptionMenu (action, sub){
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制订阅链接"); console.log(`${action} --> ${sub.name}`)
break switch (action){
case 'EDIT': case 'COPY':
this.$router.push(`/sub-edit/${encodeURIComponent(sub.name)}`); this.$clipboard(
break `${this.subscriptionBaseURL}/download/${encodeURIComponent(
case 'DELETE': sub.name)}`)
this.$store.dispatch("DELETE_SUBSCRIPTION", encodeURIComponent(sub.name)); this.$store.commit('SET_SUCCESS_MESSAGE', '成功复制订阅链接')
break break
case 'EDIT':
this.$router.push(`/sub-edit/${encodeURIComponent(sub.name)}`)
break
case 'PREVIEW':
this.previewSubName = sub.name
this.isCollectionPreview = false
this.showPreviewDialog = true
break
case 'DELETE':
this.$store.dispatch(
'DELETE_SUBSCRIPTION', encodeURIComponent(sub.name))
break
}
},
collectionMenu (action, collection){
console.log(`${action} --> ${collection.name}`)
switch (action){
case 'COPY':
this.$clipboard(
`${this.subscriptionBaseURL}/download/collection/${encodeURIComponent(
collection.name)}`)
this.$store.commit('SET_SUCCESS_MESSAGE', '成功复制订阅链接')
break
case 'EDIT':
this.$router.push(`/collection-edit/${collection.name}`)
break
case 'PREVIEW':
this.previewSubName = collection.name
this.isCollectionPreview = true
this.showPreviewDialog = true
break
case 'DELETE':
this.$store.dispatch('DELETE_COLLECTION', collection.name)
break
}
},
preview (item, type = 'sub'){
if (type === 'sub'){
this.url = `${BACKEND_BASE}/download/${encodeURIComponent(
item.name)}`
this.sub = item.url
} else{
this.url = `${BACKEND_BASE}/download/collection/${encodeURIComponent(
item.name)}`
}
this.showProxyList = true
},
createSub (){
this.$router.push('/sub-edit/UNTITLED')
},
createCol (){
this.$router.push('/collection-edit/UNTITLED')
},
async refreshProxyList (){
try{
await this.$refs.proxyList.refresh()
this.$store.commit('SET_SUCCESS_MESSAGE', '刷新成功!')
} catch (err){
this.$store.commit('SET_ERROR_MESSAGE', err.response.data.message)
}
},
getIconClass (url){
return url.indexOf(
'#invert') !== - 1 && !this.$vuetify.theme.dark ? 'invert' : ''
} }
},
collectionMenu(action, collection) {
console.log(`${action} --> ${collection.name}`);
switch (action) {
case 'COPY':
this.$clipboard(`${this.subscriptionBaseURL}/download/collection/${encodeURIComponent(collection.name)}`);
this.$store.commit("SET_SUCCESS_MESSAGE", "成功复制订阅链接");
break
case 'EDIT':
this.$router.push(`/collection-edit/${collection.name}`);
break
case 'DELETE':
this.$store.dispatch("DELETE_COLLECTION", collection.name);
break
}
},
preview(item, type = 'sub') {
if (type === 'sub') {
this.url = `${BACKEND_BASE}/download/${encodeURIComponent(item.name)}`;
this.sub = item.url;
} else {
this.url = `${BACKEND_BASE}/download/collection/${encodeURIComponent(item.name)}`
}
this.showProxyList = true;
},
createSub() {
this.$router.push("/sub-edit/UNTITLED");
},
createCol() {
this.$router.push("/collection-edit/UNTITLED")
},
async refreshProxyList() {
try {
await this.$refs.proxyList.refresh();
this.$store.commit("SET_SUCCESS_MESSAGE", "刷新成功!");
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", err.response.data.message);
}
},
getIconClass(url) {
return url.indexOf('#invert') !== -1 && !this.$vuetify.theme.dark ? 'invert' : ''
} }
} }
}
</script> </script>
<style scoped> <style scoped>
.invert { .invert {
filter: invert(100%); filter : invert(100%);
} }
.v-dialog > .v-card > .v-toolbar { .v-dialog > .v-card > .v-toolbar {
position: sticky; position : sticky;
top: 0; top : 0;
z-index: 999; z-index : 999;
} }
</style> </style>