mirror of
https://github.com/CzBiX/qb-web.git
synced 2026-04-08 21:48:21 +08:00
Store user config in local storage
This commit is contained in:
@@ -57,7 +57,7 @@ import Torrents from './components/Torrents.vue';
|
||||
import AppFooter from './components/Footer.vue';
|
||||
import LogsDialog from './components/LogsDialog.vue';
|
||||
import { api } from './Api';
|
||||
import { mapActions, mapState, mapMutations } from 'vuex';
|
||||
import { mapActions, mapGetters, mapState, mapMutations } from 'vuex';
|
||||
import Axios, { AxiosError } from 'axios';
|
||||
|
||||
export default Vue.extend({
|
||||
@@ -88,7 +88,10 @@ export default Vue.extend({
|
||||
clearTimeout(this.task);
|
||||
}
|
||||
},
|
||||
computed: mapState(['mainData', 'rid', 'config']),
|
||||
computed: {
|
||||
...mapState(['mainData', 'rid']),
|
||||
...mapGetters(['config']),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'updateMainData',
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
v-for="(child, i) in item.filterGroups"
|
||||
:key="i"
|
||||
:group="child"
|
||||
v-model="filter[child.select]"
|
||||
/>
|
||||
</template>
|
||||
<v-list-tile v-else :key="item.text" @click="item.click ? item.click() : null">
|
||||
@@ -89,11 +88,6 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
filter: {
|
||||
type: null,
|
||||
category: null,
|
||||
site: null,
|
||||
},
|
||||
basicItems: null,
|
||||
// {
|
||||
// 'icon': 'mdi-menu-up',
|
||||
@@ -179,7 +173,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations(['updateFilter']),
|
||||
async switchUi() {
|
||||
await api.switchToOldUi();
|
||||
|
||||
@@ -189,15 +182,6 @@ export default {
|
||||
this.$emit('input', Object.assign({}, this.value, {[key]: value}));
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
filter: {
|
||||
handler() {
|
||||
this.updateFilter(this.filter);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,17 +2,25 @@
|
||||
<div>
|
||||
<v-toolbar
|
||||
flat
|
||||
dense
|
||||
v-show="hasSelected"
|
||||
color="white"
|
||||
height="57px"
|
||||
>
|
||||
<v-btn icon :disabled="!hasSelected" @click="confirmDelete">
|
||||
<v-checkbox class="shrink menu-check"
|
||||
:input-value="hasSelected"
|
||||
:indeterminate="!hasSelectedAll"
|
||||
primary
|
||||
hide-details
|
||||
@click.stop="selectedRows = []"
|
||||
></v-checkbox>
|
||||
<v-btn icon @click="confirmDelete">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
<v-divider vertical inset />
|
||||
<v-btn icon :disabled="!hasSelected" @click="resumeTorrents">
|
||||
<v-btn icon @click="resumeTorrents">
|
||||
<v-icon>mdi-play</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!hasSelected" @click="pauseTorrents">
|
||||
<v-btn icon @click="pauseTorrents">
|
||||
<v-icon>mdi-pause</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
@@ -22,6 +30,7 @@
|
||||
:items="torrents"
|
||||
item-key="hash"
|
||||
:hide-actions="torrents.length <= pagination.rowsPerPage"
|
||||
v-class:hide-headers="hasSelected"
|
||||
select-all
|
||||
:pagination.sync="pagination"
|
||||
v-model="selectedRows"
|
||||
@@ -33,7 +42,7 @@
|
||||
hide-details
|
||||
/>
|
||||
</td>
|
||||
<td>{{ row.item.name }}</td>
|
||||
<td :title="row.item.name">{{ row.item.name }}</td>
|
||||
<td>{{ row.item.size | formatSize }}</td>
|
||||
<td>
|
||||
<v-progress-linear
|
||||
@@ -47,11 +56,15 @@
|
||||
<td>{{ row.item.state }}</td>
|
||||
<td>{{ row.item.num_seeds }}/{{ row.item.num_complete }}</td>
|
||||
<td>{{ row.item.num_leechs }}/{{ row.item.num_incomplete }}</td>
|
||||
<td>{{ row.item.dlspeed | formatSize }}/s</td>
|
||||
<td>{{ row.item.upspeed | formatSize }}/s</td>
|
||||
<td>{{ row.item.eta | formatDuration }}</td>
|
||||
<td>{{ formatNetworkSpeed(row.item.dlspeed) }}</td>
|
||||
<td>{{ formatNetworkSpeed(row.item.upspeed) }}</td>
|
||||
<td>{{ row.item.eta | formatDuration({dayLimit: 100}) }}</td>
|
||||
<td>{{ row.item.ratio.toFixed(2) }}</td>
|
||||
<td>{{ row.item.added_on | formatTimestamp }}</td>
|
||||
<td>
|
||||
<span :title="row.item.added_on | formatTimestamp">
|
||||
{{ row.item.added_on | formatAsDuration }} ago
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
@@ -62,9 +75,10 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import ConfirmDeleteDialog from './dialogs/ConfirmDeleteDialog.vue';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { mapState, mapGetters, mapMutations } from 'vuex';
|
||||
import _ from 'lodash';
|
||||
import { api } from '../Api';
|
||||
import { formatSize, formatDuration } from '../filters';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'torrents',
|
||||
@@ -75,8 +89,8 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
const headers = [
|
||||
{ text: 'Name', value: 'name', class: 'th-name' },
|
||||
{ text: 'Size', value: 'size' },
|
||||
{ text: 'Name', value: 'name', width: 'auto', class: 'th-name' },
|
||||
{ text: 'Size', value: 'size', width: '54px' },
|
||||
{ text: 'Progress', value: 'progress' },
|
||||
{ text: 'Status', value: 'state' },
|
||||
{ text: 'Seeds', value: 'num_complete' },
|
||||
@@ -92,22 +106,26 @@ export default Vue.extend({
|
||||
headers,
|
||||
selectedRows: [],
|
||||
deleteDialog: false,
|
||||
pagination: {
|
||||
rowsPerPage: 100,
|
||||
},
|
||||
pagination: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.pagination = this.$store.getters.config.pagination;
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState([
|
||||
'filter',
|
||||
]),
|
||||
...mapGetters([
|
||||
'isDataReady',
|
||||
'allTorrents',
|
||||
'torrentGroupByCategory',
|
||||
'torrentGroupBySite',
|
||||
]),
|
||||
...mapState({
|
||||
filter(state, getters) {
|
||||
return getters.config.filter;
|
||||
},
|
||||
}),
|
||||
hasSelected() {
|
||||
return this.selectedRows.length;
|
||||
},
|
||||
@@ -128,6 +146,9 @@ export default Vue.extend({
|
||||
|
||||
return list;
|
||||
},
|
||||
hasSelectedAll() {
|
||||
return this.hasSelected && this.selectedRows.length === Math.min(this.torrents.length, this.pagination.rowsPerPage)
|
||||
},
|
||||
},
|
||||
|
||||
filters: {
|
||||
@@ -141,6 +162,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'updateConfig',
|
||||
]),
|
||||
confirmDelete() {
|
||||
this.deleteDialog = true;
|
||||
},
|
||||
@@ -150,11 +174,38 @@ export default Vue.extend({
|
||||
async pauseTorrents() {
|
||||
await api.pauseTorrents(this.selectedHashes);
|
||||
},
|
||||
formatNetworkSpeed(speed: number) {
|
||||
if (speed === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return formatSize(speed) + '/s';
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
pagination: {
|
||||
handler() {
|
||||
this.updateConfig({
|
||||
key: 'pagination',
|
||||
value: this.pagination,
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .v-toolbar__content {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.menu-check {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::v-deep .v-datatable thead th, .v-datatable tbody td {
|
||||
padding: 0 2px !important;
|
||||
width: auto;
|
||||
@@ -172,6 +223,14 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .v-datatable {
|
||||
// table-layout: fixed;
|
||||
}
|
||||
|
||||
::v-deep.hide-headers .v-datatable thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::v-deep .v-datatable thead th.th-name {
|
||||
// max-width: 100px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<template v-slot:activator>
|
||||
<v-list-tile>
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title v-class:primary--text="value !== null">
|
||||
<v-list-tile-title v-class:primary--text="selected !== null">
|
||||
{{ group.title }}
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
@@ -15,7 +15,7 @@
|
||||
<v-list-tile
|
||||
v-for="(child, i) in group.children"
|
||||
:key="i"
|
||||
v-class:primary--text="value === child.key"
|
||||
v-class:primary--text="selected === child.key"
|
||||
@click.stop="select(child.key)"
|
||||
>
|
||||
<v-list-tile-action>
|
||||
@@ -44,20 +44,31 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
group: Object,
|
||||
value: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: this.value,
|
||||
};
|
||||
selected: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.selected = this.$store.getters.config.filter[this.group.select];
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'updateConfig',
|
||||
]),
|
||||
select(key: any) {
|
||||
this.selected = this.selected === key ? null : key;
|
||||
this.$emit('input', this.selected);
|
||||
this.updateConfig({
|
||||
key: 'filter',
|
||||
value: {
|
||||
[this.group.select]: this.selected
|
||||
},
|
||||
});
|
||||
},
|
||||
isFontIcon(icon: string) {
|
||||
return icon.startsWith('mdi-');
|
||||
|
||||
@@ -17,7 +17,12 @@ export function formatSize(value: number) {
|
||||
|
||||
Vue.filter('formatSize', formatSize);
|
||||
|
||||
Vue.filter('formatDuration', (value: number) => {
|
||||
export interface DurationOptions {
|
||||
dayLimit?: number;
|
||||
maxUnitSize?: number;
|
||||
}
|
||||
|
||||
export function formatDuration(value: number, options?: DurationOptions) {
|
||||
const minute = 60;
|
||||
const hour = 3600;
|
||||
const day = 3600 * 24;
|
||||
@@ -29,8 +34,15 @@ Vue.filter('formatDuration', (value: number) => {
|
||||
let unitSize = 0;
|
||||
const parts = [];
|
||||
|
||||
const defaultOptions: DurationOptions = {
|
||||
maxUnitSize: 1,
|
||||
dayLimit: 0,
|
||||
};
|
||||
|
||||
const opt = options ? Object.assign(defaultOptions, options) : defaultOptions;
|
||||
|
||||
while (true) {
|
||||
if (unitSize === 2 || index === durations.length) {
|
||||
if ((opt.maxUnitSize && unitSize === opt.maxUnitSize) || index === durations.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -40,7 +52,7 @@ Vue.filter('formatDuration', (value: number) => {
|
||||
continue;
|
||||
}
|
||||
const result = Math.floor(value / duration);
|
||||
if (index === 0 && result >= 100) {
|
||||
if (index === 0 && opt.dayLimit && result >= opt.dayLimit) {
|
||||
return '∞';
|
||||
}
|
||||
parts.push(result + units[index]);
|
||||
@@ -50,13 +62,15 @@ Vue.filter('formatDuration', (value: number) => {
|
||||
unitSize++;
|
||||
}
|
||||
|
||||
if (unitSize < 2 && index !== durations.length) {
|
||||
const result = Math.floor(value / durations[index]);
|
||||
parts.push(result + units[index]);
|
||||
}
|
||||
// if (unitSize < 2 && index !== durations.length) {
|
||||
// const result = Math.floor(value / durations[index]);
|
||||
// parts.push(result + units[index]);
|
||||
// }
|
||||
|
||||
return parts.join(' ');
|
||||
});
|
||||
};
|
||||
|
||||
Vue.filter('formatDuration', formatDuration);
|
||||
|
||||
Vue.filter('formatTimestamp', (timestamp: number) => {
|
||||
if (timestamp === null) {
|
||||
@@ -66,3 +80,10 @@ Vue.filter('formatTimestamp', (timestamp: number) => {
|
||||
const m = dayjs.unix(timestamp);
|
||||
return m.format('YYYY-MM-DD HH:mm:ss');
|
||||
});
|
||||
|
||||
export function formatAsDuration(date: number, options?: DurationOptions) {
|
||||
const duration = (Date.now() / 1000) - date;
|
||||
return formatDuration(duration, options);
|
||||
};
|
||||
|
||||
Vue.filter('formatAsDuration', formatAsDuration);
|
||||
|
||||
48
src/store.ts
48
src/store.ts
@@ -4,18 +4,38 @@ import _ from 'lodash';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const defaultConfig = {
|
||||
updateInterval: 2000,
|
||||
pagination: {
|
||||
rowsPerPage: 100,
|
||||
},
|
||||
filter: {
|
||||
type: null,
|
||||
category: null,
|
||||
site: null,
|
||||
},
|
||||
};
|
||||
|
||||
const configKey = 'qb-config';
|
||||
|
||||
function saveConfig(obj: any) {
|
||||
localStorage[configKey] = JSON.stringify(obj);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
const tmp = localStorage[configKey];
|
||||
if (!tmp) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(tmp);
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
rid: 0,
|
||||
mainData: null,
|
||||
filter: {
|
||||
type: null,
|
||||
category: null,
|
||||
site: null,
|
||||
},
|
||||
config: {
|
||||
updateInterval: 2000,
|
||||
},
|
||||
userConfig: loadConfig(),
|
||||
preferences: null,
|
||||
},
|
||||
mutations: {
|
||||
@@ -37,11 +57,19 @@ export default new Vuex.Store({
|
||||
updatePreferences(state, payload) {
|
||||
state.preferences = payload;
|
||||
},
|
||||
updateFilter(state, payload) {
|
||||
state.filter = _.clone(payload);
|
||||
updateConfig(state, payload) {
|
||||
const key = payload.key;
|
||||
const value = payload.value;
|
||||
const tmp = _.merge({}, state.userConfig[key], value);
|
||||
Vue.set(state.userConfig, key, tmp);
|
||||
|
||||
saveConfig(state.userConfig);
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
config(state) {
|
||||
return _.merge({}, defaultConfig, state.userConfig);
|
||||
},
|
||||
isDataReady(state) {
|
||||
return !!state.mainData;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user