mirror of
https://github.com/CzBiX/qb-web.git
synced 2026-04-13 18:01:17 +08:00
Tweak UI
This commit is contained in:
64
src/App.vue
64
src/App.vue
@@ -8,32 +8,9 @@
|
||||
width="300"
|
||||
>
|
||||
<drawer v-model="drawerOptions" />
|
||||
<template v-if="phoneLayout">
|
||||
<v-spacer />
|
||||
<v-divider />
|
||||
<v-expansion-panels
|
||||
class="drawer-footer"
|
||||
>
|
||||
<v-expansion-panel
|
||||
lazy
|
||||
@input="drawerFooterOpen"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon class="footer-icon shrink">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
<span class="footer-title">
|
||||
Status info
|
||||
</span>
|
||||
</div>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<app-footer phone-layout />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<div ref="end" />
|
||||
</v-expansion-panels>
|
||||
|
||||
<template #append>
|
||||
<DrawerFooter />
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
<main-toolbar v-model="drawer" />
|
||||
@@ -87,9 +64,9 @@ import Torrents from './components/Torrents.vue';
|
||||
import AppFooter from './components/Footer.vue';
|
||||
import LogsDialog from './components/dialogs/LogsDialog.vue';
|
||||
import RssDialog from './components/dialogs/RssDialog.vue';
|
||||
import DrawerFooter from './components/drawer/DrawerFooter.vue';
|
||||
|
||||
import api from './Api';
|
||||
import { timeout } from './utils';
|
||||
import Component from 'vue-class-component';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
import { MainData } from './types';
|
||||
@@ -108,6 +85,7 @@ let appWrapEl: HTMLElement;
|
||||
GlobalDialog,
|
||||
GlobalSnackBar,
|
||||
RssDialog,
|
||||
DrawerFooter,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
@@ -203,18 +181,6 @@ export default class App extends Vue {
|
||||
this.task = setTimeout(this.getMainData, this.config.updateInterval);
|
||||
}
|
||||
|
||||
async drawerFooterOpen(v: boolean) {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
await timeout(3000);
|
||||
|
||||
(this.$refs.end as HTMLElement).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
onPaste(e: ClipboardEvent) {
|
||||
if ((e.target as HTMLElement).tagName === 'INPUT') {
|
||||
return;
|
||||
@@ -238,26 +204,6 @@ export default class App extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.phone-layout ::v-deep .v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.drawer-footer .v-expansion-panel-header {
|
||||
padding: 12px 16px 12px 16px;
|
||||
|
||||
.footer-icon {
|
||||
font-size: 22px;
|
||||
margin-left: 10px;
|
||||
margin-right: 34px;
|
||||
}
|
||||
|
||||
.footer-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-footer {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="add-form">
|
||||
<v-btn
|
||||
fab
|
||||
bottom
|
||||
color="primary"
|
||||
fixed
|
||||
right
|
||||
small
|
||||
@click="dialog = !dialog"
|
||||
class="btn-add"
|
||||
:class="{'with-footer': $vuetify.breakpoint.smAndUp}"
|
||||
|
||||
@@ -35,22 +35,6 @@
|
||||
:value="searchQuery"
|
||||
/>
|
||||
<v-spacer v-if="!phoneLayout" />
|
||||
<v-btn
|
||||
icon
|
||||
@click="toggleDarkMode"
|
||||
>
|
||||
<v-icon v-text="darkModeIcon" />
|
||||
</v-btn>
|
||||
<v-select
|
||||
v-show="!searchBarExpanded"
|
||||
class="locales"
|
||||
:items="locales"
|
||||
prepend-inner-icon="mdi-translate"
|
||||
v-model="currentLocale"
|
||||
hide-details
|
||||
solo
|
||||
flat
|
||||
/>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
@@ -59,16 +43,13 @@ import { throttle } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { mapMutations } from 'vuex';
|
||||
|
||||
import i18n, { tr, translations, defaultLocale } from '@/locale';
|
||||
import { DialogType, DialogConfig, SnackBarConfig, ConfigPayload } from '@/store/types';
|
||||
import Component from 'vue-class-component';
|
||||
import { Prop, Emit, Watch } from 'vue-property-decorator';
|
||||
import { Prop, Emit } from 'vue-property-decorator';
|
||||
import { ConfigPayload } from '@/store/types';
|
||||
|
||||
@Component({
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'showDialog',
|
||||
'showSnackBar',
|
||||
'updateConfig',
|
||||
]),
|
||||
},
|
||||
@@ -77,19 +58,10 @@ export default class MainToolbar extends Vue {
|
||||
@Prop(Boolean)
|
||||
readonly value!: boolean
|
||||
|
||||
showDialog!: (_: DialogConfig) => void
|
||||
showSnackBar!: (_: SnackBarConfig) => void
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
|
||||
locales = this.buildLocales()
|
||||
currentLocale = i18n.locale()
|
||||
oldLocale = this.currentLocale
|
||||
focusedSearch = false
|
||||
|
||||
get darkModeIcon() {
|
||||
return this.$vuetify.theme.dark ? 'mdi-brightness-4' : 'mdi-brightness-6';
|
||||
}
|
||||
|
||||
get searchQuery() {
|
||||
return this.$store.getters.config.filter.query;
|
||||
}
|
||||
@@ -102,30 +74,13 @@ export default class MainToolbar extends Vue {
|
||||
return this.phoneLayout && (this.focusedSearch || !!this.searchQuery);
|
||||
}
|
||||
|
||||
buildLocales() {
|
||||
const locales: {}[] = Object.entries(translations).map(([lang, translation]) => {
|
||||
return {
|
||||
text: translation.lang,
|
||||
value: lang,
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
text: tr('auto'),
|
||||
value: null,
|
||||
},
|
||||
...locales
|
||||
]
|
||||
}
|
||||
|
||||
@Emit('input')
|
||||
toggle() {
|
||||
return !this.value;
|
||||
}
|
||||
|
||||
onSearch = throttle(async (v: string) => {
|
||||
// avoid hang input
|
||||
// avoid input lag
|
||||
await this.$nextTick();
|
||||
this.updateConfig({
|
||||
key: 'filter',
|
||||
@@ -134,54 +89,6 @@ export default class MainToolbar extends Vue {
|
||||
},
|
||||
});
|
||||
}, 400)
|
||||
|
||||
async switchLocale(locale: keyof typeof translations | null) {
|
||||
if (locale === this.oldLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await new Promise((resolve) => {
|
||||
const localeKey = !locale ? defaultLocale : locale
|
||||
this.showDialog({
|
||||
content: {
|
||||
text: tr('dialog.switch_locale.msg', { lang: translations[localeKey].lang }),
|
||||
type: DialogType.OkCancel,
|
||||
callback: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
this.currentLocale = this.oldLocale;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('updateConfig', {
|
||||
key: 'locale',
|
||||
value: locale,
|
||||
});
|
||||
|
||||
this.showSnackBar({
|
||||
text: tr('label.reloading'),
|
||||
})
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
toggleDarkMode() {
|
||||
const { theme } = this.$vuetify;
|
||||
theme.dark = !theme.dark;
|
||||
|
||||
this.updateConfig({
|
||||
key: 'darkMode',
|
||||
value: theme.dark,
|
||||
});
|
||||
}
|
||||
|
||||
@Watch('currentLocale')
|
||||
onCurrentLocaleChanged(v: keyof typeof translations) {
|
||||
this.switchLocale(v)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -207,26 +114,6 @@ export default class MainToolbar extends Vue {
|
||||
flex: 1;
|
||||
margin: 0 0.5em 0 1em;
|
||||
}
|
||||
|
||||
.locales ::v-deep .v-select__slot {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix width
|
||||
// see: https://github.com/vuetifyjs/vuetify/issues/6275
|
||||
.locales {
|
||||
flex-grow: 0;
|
||||
|
||||
&::v-deep .v-select__selections {
|
||||
.v-select__selection {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -610,7 +610,19 @@ export default class Torrents extends Vue {
|
||||
}
|
||||
|
||||
::v-deep .v-data-footer {
|
||||
margin-right: 6em;
|
||||
margin-right: 4em;
|
||||
|
||||
.phone-layout & {
|
||||
justify-content: flex-start;
|
||||
|
||||
.v-data-footer__select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-data-footer__pagination {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
180
src/components/drawer/DrawerFooter.vue
Normal file
180
src/components/drawer/DrawerFooter.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="drawer-footer">
|
||||
<v-expand-transition v-if="showInfo">
|
||||
<div>
|
||||
<v-divider />
|
||||
|
||||
<AppFooter
|
||||
phone-layout
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="button-bar">
|
||||
<template v-if="phoneLayout">
|
||||
<v-btn
|
||||
icon
|
||||
@click="showInfo = !showInfo"
|
||||
>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-menu>
|
||||
<template #activator="{ on }">
|
||||
<v-btn
|
||||
icon
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-translate</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item-group
|
||||
v-model="currentLocale"
|
||||
color="primary"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="item in locales"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
<v-list-item-title>{{ item.text }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
@click="toggleDarkMode"
|
||||
>
|
||||
<v-icon v-text="darkModeIcon" />
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import Component from 'vue-class-component';
|
||||
import { mapMutations } from 'vuex';
|
||||
import { Watch } from 'vue-property-decorator';
|
||||
|
||||
import i18n, { tr, translations, defaultLocale, LocaleKey } from '@/locale';
|
||||
import { DialogType, DialogConfig, SnackBarConfig, ConfigPayload } from '@/store/types';
|
||||
import AppFooter from '@/components/Footer.vue';
|
||||
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
AppFooter,
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'showDialog',
|
||||
'showSnackBar',
|
||||
'updateConfig',
|
||||
]),
|
||||
},
|
||||
})
|
||||
export default class DrawerFooter extends Vue {
|
||||
locales = this.buildLocales()
|
||||
currentLocale = i18n.locale()
|
||||
oldLocale = this.currentLocale
|
||||
showInfo = false
|
||||
|
||||
showDialog!: (_: DialogConfig) => void
|
||||
showSnackBar!: (_: SnackBarConfig) => void
|
||||
updateConfig!: (_: ConfigPayload) => void
|
||||
|
||||
get darkModeIcon() {
|
||||
return this.$vuetify.theme.dark ? 'mdi-brightness-4' : 'mdi-brightness-7';
|
||||
}
|
||||
|
||||
get phoneLayout() {
|
||||
return this.$vuetify.breakpoint.xsOnly;
|
||||
}
|
||||
|
||||
buildLocales() {
|
||||
const locales: {}[] = Object.entries(translations).map(([lang, translation]) => {
|
||||
return {
|
||||
text: translation.lang,
|
||||
value: lang,
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
text: tr('auto'),
|
||||
value: null,
|
||||
},
|
||||
...locales
|
||||
]
|
||||
}
|
||||
|
||||
async switchLocale(locale: LocaleKey) {
|
||||
if (locale === this.oldLocale) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await new Promise((resolve) => {
|
||||
const localeKey = !locale ? defaultLocale : locale
|
||||
this.showDialog({
|
||||
content: {
|
||||
text: tr('dialog.switch_locale.msg', { lang: translations[localeKey].lang }),
|
||||
type: DialogType.OkCancel,
|
||||
callback: resolve,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
this.currentLocale = this.oldLocale;
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateConfig({
|
||||
key: 'locale',
|
||||
value: locale,
|
||||
});
|
||||
|
||||
this.showSnackBar({
|
||||
text: tr('label.reloading'),
|
||||
})
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
toggleDarkMode() {
|
||||
const { theme } = this.$vuetify;
|
||||
theme.dark = !theme.dark;
|
||||
|
||||
this.updateConfig({
|
||||
key: 'darkMode',
|
||||
value: theme.dark,
|
||||
});
|
||||
}
|
||||
|
||||
@Watch('currentLocale')
|
||||
onCurrentLocaleChanged(v: LocaleKey) {
|
||||
this.switchLocale(v)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-bar {
|
||||
display: flex;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,8 @@ export const translations = {
|
||||
'zh-CN': zhCn,
|
||||
}
|
||||
|
||||
export type LocaleKey = keyof typeof translations | null;
|
||||
|
||||
const polyglot = new Polyglot({
|
||||
phrases: translations.en
|
||||
});
|
||||
@@ -18,7 +20,7 @@ function matchLocale() {
|
||||
|
||||
for (const code of languages) {
|
||||
if (code in translations) {
|
||||
return code as keyof typeof translations;
|
||||
return (code as LocaleKey)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +30,7 @@ function matchLocale() {
|
||||
export const defaultLocale = matchLocale()
|
||||
|
||||
function updateLocale() {
|
||||
let locale: keyof typeof translations | undefined | null = loadConfig()['locale'];
|
||||
let locale: LocaleKey | undefined = loadConfig()['locale'];
|
||||
|
||||
if (!locale) {
|
||||
locale = defaultLocale;
|
||||
|
||||
Reference in New Issue
Block a user