Store user config in local storage

This commit is contained in:
CzBiX
2019-04-15 19:01:56 +08:00
parent 7f169e621c
commit 8a8c2bbf92
6 changed files with 166 additions and 60 deletions

View File

@@ -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',

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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-');

View File

@@ -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);

View File

@@ -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;
},