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

View File

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