クライアントの設定コンポーネントを整理
* デスクトップとモバイルで統一 * いくつかの設定を廃止
This commit is contained in:
82
src/client/app/common/views/components/settings/2fa.vue
Normal file
82
src/client/app/common/views/components/settings/2fa.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="2fa">
|
||||
<p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p>
|
||||
<ui-info warn>{{ $t('caution') }}</ui-info>
|
||||
<p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p>
|
||||
<template v-if="$store.state.i.twoFactorEnabled">
|
||||
<p>{{ $t('already-registered') }}</p>
|
||||
<ui-button @click="unregister">{{ $t('unregister') }}</ui-button>
|
||||
</template>
|
||||
<div v-if="data && !$store.state.i.twoFactorEnabled">
|
||||
<ol>
|
||||
<li>{{ $t('authenticator') }}<a href="https://support.google.com/accounts/answer/1066447" target="_blank">{{ $t('howtoinstall') }}</a></li>
|
||||
<li>{{ $t('scan') }}<br><img :src="data.qr"></li>
|
||||
<li>{{ $t('done') }}<br>
|
||||
<ui-input v-model="token">{{ $t('token') }}</ui-input>
|
||||
<ui-button primary @click="submit">{{ $t('submit') }}</ui-button>
|
||||
</li>
|
||||
</ol>
|
||||
<ui-info>{{ $t('info') }}</ui-info>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/settings.2fa.vue'),
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
token: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
register() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/2fa/register', {
|
||||
password: password
|
||||
}).then(data => {
|
||||
this.data = data;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unregister() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/2fa/unregister', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
this.$notify(this.$t('unregistered'));
|
||||
this.$store.state.i.twoFactorEnabled = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.$root.api('i/2fa/done', {
|
||||
token: this.token
|
||||
}).then(() => {
|
||||
this.$notify(this.$t('success'));
|
||||
this.$store.state.i.twoFactorEnabled = true;
|
||||
}).catch(() => {
|
||||
this.$notify(this.$t('failed'));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
78
src/client/app/common/views/components/settings/api.vue
Normal file
78
src/client/app/common/views/components/settings/api.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="key"/> API</template>
|
||||
|
||||
<section class="fit-top">
|
||||
<ui-input :value="$store.state.i.token" readonly>
|
||||
<span>{{ $t('token') }}</span>
|
||||
</ui-input>
|
||||
<p>{{ $t('intro') }}</p>
|
||||
<ui-info warn>{{ $t('caution') }}</ui-info>
|
||||
<p>{{ $t('regeneration-of-token') }}</p>
|
||||
<ui-button @click="regenerateToken"><fa icon="sync-alt"/> {{ $t('regenerate-token') }}</ui-button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
|
||||
<ui-input v-model="endpoint">
|
||||
<span>{{ $t('console.endpoint') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-model="body">
|
||||
<span>{{ $t('console.parameter') }} (JSON or JSON5)</span>
|
||||
<template #desc>{{ $t('console.credential-info') }}</template>
|
||||
</ui-textarea>
|
||||
<ui-button @click="send" :disabled="sending">
|
||||
<template v-if="sending">{{ $t('console.sending') }}</template>
|
||||
<template v-else><fa icon="paper-plane"/> {{ $t('console.send') }}</template>
|
||||
</ui-button>
|
||||
<ui-textarea v-if="res" v-model="res" readonly tall>
|
||||
<span>{{ $t('console.response') }}</span>
|
||||
</ui-textarea>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import * as JSON5 from 'json5';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/api-settings.vue'),
|
||||
data() {
|
||||
return {
|
||||
endpoint: '',
|
||||
body: '{}',
|
||||
res: null,
|
||||
sending: false
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
regenerateToken() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/regenerate_token', {
|
||||
password: password
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
send() {
|
||||
this.sending = true;
|
||||
this.$root.api(this.endpoint, JSON5.parse(this.body)).then(res => {
|
||||
this.sending = false;
|
||||
this.res = JSON5.stringify(res, null, 2);
|
||||
}, err => {
|
||||
this.sending = false;
|
||||
this.res = JSON5.stringify(err, null, 2);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
39
src/client/app/common/views/components/settings/apps.vue
Normal file
39
src/client/app/common/views/components/settings/apps.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="root">
|
||||
<ui-info v-if="!fetching && apps.length == 0">{{ $t('no-apps') }}</ui-info>
|
||||
<div class="apps" v-if="apps.length != 0">
|
||||
<div v-for="app in apps">
|
||||
<p><b>{{ app.name }}</b></p>
|
||||
<p>{{ app.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/settings.apps.vue'),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
apps: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.api('i/authorized_apps').then(apps => {
|
||||
this.apps = apps;
|
||||
this.fetching = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.root
|
||||
> .apps
|
||||
> div
|
||||
padding 16px 0 0 0
|
||||
border-bottom solid 1px #eee
|
||||
</style>
|
172
src/client/app/common/views/components/settings/drive.vue
Normal file
172
src/client/app/common/views/components/settings/drive.vue
Normal file
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="cloud"/> {{ $t('@.drive') }}</template>
|
||||
|
||||
<section v-if="!fetching" class="juakhbxthdewydyreaphkepoxgxvfogn">
|
||||
<div class="meter"><div :style="meterStyle"></div></div>
|
||||
<p>{{ $t('max') }}: <b>{{ capacity | bytes }}</b> {{ $t('in-use') }}: <b>{{ usage | bytes }}</b></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('stats') }}</header>
|
||||
<div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/drive-settings.vue'),
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
usage: null,
|
||||
capacity: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
meterStyle(): any {
|
||||
return {
|
||||
width: `${this.usage / this.capacity * 100}%`,
|
||||
background: tinycolor({
|
||||
h: 180 - (this.usage / this.capacity * 180),
|
||||
s: 0.7,
|
||||
l: 0.5
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$root.api('drive').then(info => {
|
||||
this.capacity = info.capacity;
|
||||
this.usage = info.usage;
|
||||
this.fetching = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.renderChart();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderChart() {
|
||||
this.$root.api('charts/user/drive', {
|
||||
userId: this.$store.state.i.id,
|
||||
span: 'day',
|
||||
limit: 21
|
||||
}).then(stats => {
|
||||
const addition = [];
|
||||
const deletion = [];
|
||||
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth();
|
||||
const d = now.getDate();
|
||||
|
||||
for (let i = 0; i < 21; i++) {
|
||||
const x = new Date(y, m, d - i);
|
||||
addition.push([
|
||||
x,
|
||||
stats.incSize[i]
|
||||
]);
|
||||
deletion.push([
|
||||
x,
|
||||
-stats.decSize[i]
|
||||
]);
|
||||
}
|
||||
|
||||
const chart = new ApexCharts(this.$refs.chart, {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
stacked: true,
|
||||
height: 150,
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '90%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
tooltip: {
|
||||
shared: true,
|
||||
intersect: false
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
series: [{
|
||||
name: 'Additions',
|
||||
data: addition
|
||||
}, {
|
||||
name: 'Deletions',
|
||||
data: deletion
|
||||
}],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
style: {
|
||||
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
axisTicks: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
crosshairs: {
|
||||
width: 1,
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: v => Vue.filter('bytes')(v, 0),
|
||||
style: {
|
||||
color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.juakhbxthdewydyreaphkepoxgxvfogn
|
||||
> .meter
|
||||
$size = 12px
|
||||
|
||||
margin-bottom 16px
|
||||
background rgba(0, 0, 0, 0.1)
|
||||
border-radius ($size / 2)
|
||||
overflow hidden
|
||||
|
||||
> div
|
||||
height $size
|
||||
border-radius ($size / 2)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
|
||||
</style>
|
114
src/client/app/common/views/components/settings/integration.vue
Normal file
114
src/client/app/common/views/components/settings/integration.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<ui-card v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
|
||||
<template #title><fa icon="share-alt"/> {{ $t('title') }}</template>
|
||||
|
||||
<section v-if="enableTwitterIntegration">
|
||||
<header><fa :icon="['fab', 'twitter']"/> Twitter</header>
|
||||
<p v-if="$store.state.i.twitter">{{ $t('connected-to') }}: <a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
|
||||
<ui-button v-if="$store.state.i.twitter" @click="disconnectTwitter">{{ $t('disconnect') }}</ui-button>
|
||||
<ui-button v-else @click="connectTwitter">{{ $t('connect') }}</ui-button>
|
||||
</section>
|
||||
|
||||
<section v-if="enableDiscordIntegration">
|
||||
<header><fa :icon="['fab', 'discord']"/> Discord</header>
|
||||
<p v-if="$store.state.i.discord">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p>
|
||||
<ui-button v-if="$store.state.i.discord" @click="disconnectDiscord">{{ $t('disconnect') }}</ui-button>
|
||||
<ui-button v-else @click="connectDiscord">{{ $t('connect') }}</ui-button>
|
||||
</section>
|
||||
|
||||
<section v-if="enableGithubIntegration">
|
||||
<header><fa :icon="['fab', 'github']"/> GitHub</header>
|
||||
<p v-if="$store.state.i.github">{{ $t('connected-to') }}: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
|
||||
<ui-button v-if="$store.state.i.github" @click="disconnectGithub">{{ $t('disconnect') }}</ui-button>
|
||||
<ui-button v-else @click="connectGithub">{{ $t('connect') }}</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { apiUrl } from '../../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/integration-settings.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
apiUrl,
|
||||
twitterForm: null,
|
||||
discordForm: null,
|
||||
githubForm: null,
|
||||
enableTwitterIntegration: false,
|
||||
enableDiscordIntegration: false,
|
||||
enableGithubIntegration: false,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
this.enableGithubIntegration = meta.enableGithubIntegration;
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.cookie = `i=${this.$store.state.i.token}`;
|
||||
this.$watch('$store.state.i', () => {
|
||||
if (this.$store.state.i.twitter) {
|
||||
if (this.twitterForm) this.twitterForm.close();
|
||||
}
|
||||
if (this.$store.state.i.discord) {
|
||||
if (this.discordForm) this.discordForm.close();
|
||||
}
|
||||
if (this.$store.state.i.github) {
|
||||
if (this.githubForm) this.githubForm.close();
|
||||
}
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
connectTwitter() {
|
||||
this.twitterForm = window.open(apiUrl + '/connect/twitter',
|
||||
'twitter_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectTwitter() {
|
||||
window.open(apiUrl + '/disconnect/twitter',
|
||||
'twitter_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
connectDiscord() {
|
||||
this.discordForm = window.open(apiUrl + '/connect/discord',
|
||||
'discord_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectDiscord() {
|
||||
window.open(apiUrl + '/disconnect/discord',
|
||||
'discord_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
connectGithub() {
|
||||
this.githubForm = window.open(apiUrl + '/connect/github',
|
||||
'github_connect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
|
||||
disconnectGithub() {
|
||||
window.open(apiUrl + '/disconnect/github',
|
||||
'github_disconnect_window',
|
||||
'height=570, width=520');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
54
src/client/app/common/views/components/settings/language.vue
Normal file
54
src/client/app/common/views/components/settings/language.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="language"/> {{ $t('title') }}</template>
|
||||
|
||||
<section class="fit-top">
|
||||
<ui-select v-model="lang" :placeholder="$t('pick-language')">
|
||||
<optgroup :label="$t('recommended')">
|
||||
<option value="">{{ $t('auto') }}</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup :label="$t('specify-language')">
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
<ui-info>Current: <i>{{ currentLanguage }}</i></ui-info>
|
||||
<ui-info warn>{{ $t('info') }}</ui-info>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { langs } from '../../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/language-settings.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
langs,
|
||||
currentLanguage: 'Unknown',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
lang: {
|
||||
get() { return this.$store.state.device.lang; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
try {
|
||||
const locale = JSON.parse(localStorage.getItem('locale') || "{}");
|
||||
const localeKey = localStorage.getItem('localeKey');
|
||||
this.currentLanguage = `${locale.meta.lang} (${localeKey})`;
|
||||
} catch { }
|
||||
},
|
||||
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="ban"/> {{ $t('mute-and-block') }}</template>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('mute') }}</header>
|
||||
<ui-info v-if="!muteFetching && mute.length == 0">{{ $t('no-muted-users') }}</ui-info>
|
||||
<div class="users" v-if="mute.length != 0">
|
||||
<div v-for="user in mute" :key="user.id">
|
||||
<p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('block') }}</header>
|
||||
<ui-info v-if="!blockFetching && block.length == 0">{{ $t('no-blocked-users') }}</ui-info>
|
||||
<div class="users" v-if="block.length != 0">
|
||||
<div v-for="user in block" :key="user.id">
|
||||
<p><b><mk-user-name :user="user"/></b> @{{ user | acct }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('word-mute') }}</header>
|
||||
<ui-textarea v-model="mutedWords">
|
||||
{{ $t('muted-words') }}<template #desc>{{ $t('muted-words-description') }}</template>
|
||||
</ui-textarea>
|
||||
<ui-button @click="save">{{ $t('save') }}</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/mute-and-block.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
muteFetching: true,
|
||||
blockFetching: true,
|
||||
mute: [],
|
||||
block: [],
|
||||
mutedWords: ''
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
_mutedWords: {
|
||||
get() { return this.$store.state.settings.mutedWords; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'mutedWords', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.mutedWords = this._mutedWords.map(words => words.join(' ')).join('\n');
|
||||
|
||||
this.$root.api('mute/list').then(mute => {
|
||||
this.mute = mute.map(x => x.mutee);
|
||||
this.muteFetching = false;
|
||||
});
|
||||
|
||||
this.$root.api('blocking/list').then(blocking => {
|
||||
this.block = blocking.map(x => x.blockee);
|
||||
this.blockFetching = false;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
this._mutedWords = this.mutedWords.split('\n').map(line => line.split(' ').filter(x => x != ''));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template>
|
||||
<section>
|
||||
<ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch">
|
||||
{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
|
||||
</ui-switch>
|
||||
<section>
|
||||
<ui-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</ui-button>
|
||||
<ui-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</ui-button>
|
||||
<ui-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</ui-button>
|
||||
</section>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/notification-settings.vue'),
|
||||
|
||||
methods: {
|
||||
onChangeAutoWatch(v) {
|
||||
this.$root.api('i/update', {
|
||||
autoWatch: v
|
||||
});
|
||||
},
|
||||
|
||||
readAllUnreadNotes() {
|
||||
this.$root.api('i/read_all_unread_notes');
|
||||
},
|
||||
|
||||
readAllMessagingMessages() {
|
||||
this.$root.api('i/read_all_messaging_messages');
|
||||
},
|
||||
|
||||
readAllNotifications() {
|
||||
this.$root.api('notifications/mark_all_as_read');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
63
src/client/app/common/views/components/settings/password.vue
Normal file
63
src/client/app/common/views/components/settings/password.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-button @click="reset">{{ $t('reset') }}</ui-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/password-settings.vue'),
|
||||
methods: {
|
||||
async reset() {
|
||||
const { canceled: canceled1, result: currentPassword } = await this.$root.dialog({
|
||||
title: this.$t('enter-current-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled1) return;
|
||||
|
||||
const { canceled: canceled2, result: newPassword } = await this.$root.dialog({
|
||||
title: this.$t('enter-new-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled2) return;
|
||||
|
||||
const { canceled: canceled3, result: newPassword2 } = await this.$root.dialog({
|
||||
title: this.$t('enter-new-password-again'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled3) return;
|
||||
|
||||
if (newPassword !== newPassword2) {
|
||||
this.$root.dialog({
|
||||
title: null,
|
||||
text: this.$t('not-match')
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$root.api('i/change_password', {
|
||||
currentPassword,
|
||||
newPassword
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('changed')
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('failed')
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
337
src/client/app/common/views/components/settings/profile.vue
Normal file
337
src/client/app/common/views/components/settings/profile.vue
Normal file
@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="user"/> {{ $t('title') }}</template>
|
||||
|
||||
<section class="esokaraujimuwfttfzgocmutcihewscl">
|
||||
<div class="header" :style="bannerStyle">
|
||||
<mk-avatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true"/>
|
||||
</div>
|
||||
|
||||
<ui-form :disabled="saving">
|
||||
<ui-input v-model="name" :max="30">
|
||||
<span>{{ $t('name') }}</span>
|
||||
</ui-input>
|
||||
|
||||
<ui-input v-model="username" readonly>
|
||||
<span>{{ $t('account') }}</span>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</ui-input>
|
||||
|
||||
<ui-input v-model="location">
|
||||
<span>{{ $t('location') }}</span>
|
||||
<template #prefix><fa icon="map-marker-alt"/></template>
|
||||
</ui-input>
|
||||
|
||||
<ui-input v-model="birthday" type="date">
|
||||
<template #title>{{ $t('birthday') }}</template>
|
||||
<template #prefix><fa icon="birthday-cake"/></template>
|
||||
</ui-input>
|
||||
|
||||
<ui-textarea v-model="description" :max="500">
|
||||
<span>{{ $t('description') }}</span>
|
||||
<template #desc>{{ $t('you-can-include-hashtags') }}</template>
|
||||
</ui-textarea>
|
||||
|
||||
<ui-select v-model="lang">
|
||||
<template #label>{{ $t('language') }}</template>
|
||||
<template #icon><fa icon="language"/></template>
|
||||
<option v-for="lang in unique(Object.values(langmap).map(x => x.nativeName)).map(name => Object.keys(langmap).find(k => langmap[k].nativeName == name))" :value="lang" :key="lang">{{ langmap[lang].nativeName }}</option>
|
||||
</ui-select>
|
||||
|
||||
<ui-input type="file" @change="onAvatarChange">
|
||||
<span>{{ $t('avatar') }}</span>
|
||||
<template #icon><fa icon="image"/></template>
|
||||
<template #desc v-if="avatarUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
|
||||
</ui-input>
|
||||
|
||||
<ui-input type="file" @change="onBannerChange">
|
||||
<span>{{ $t('banner') }}</span>
|
||||
<template #icon><fa icon="image"/></template>
|
||||
<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
|
||||
</ui-input>
|
||||
|
||||
<ui-button @click="save(true)">{{ $t('save') }}</ui-button>
|
||||
</ui-form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('advanced') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isCat" @change="save(false)">{{ $t('is-cat') }}</ui-switch>
|
||||
<ui-switch v-model="isBot" @change="save(false)">{{ $t('is-bot') }}</ui-switch>
|
||||
<ui-switch v-model="alwaysMarkNsfw">{{ $t('@._settings.always-mark-nsfw') }}</ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('privacy') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-switch v-model="isLocked" @change="save(false)">{{ $t('is-locked') }}</ui-switch>
|
||||
<ui-switch v-model="carefulBot" :disabled="isLocked" @change="save(false)">{{ $t('careful-bot') }}</ui-switch>
|
||||
<ui-switch v-model="autoAcceptFollowed" :disabled="!isLocked && !carefulBot" @change="save(false)">{{ $t('auto-accept-followed') }}</ui-switch>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="enableEmail">
|
||||
<header>{{ $t('email') }}</header>
|
||||
|
||||
<div>
|
||||
<template v-if="$store.state.i.email != null">
|
||||
<ui-info v-if="$store.state.i.emailVerified">{{ $t('email-verified') }}</ui-info>
|
||||
<ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
|
||||
</template>
|
||||
<ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
|
||||
<ui-button @click="updateEmail()">{{ $t('save') }}</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('export') }}</header>
|
||||
|
||||
<div>
|
||||
<ui-select v-model="exportTarget">
|
||||
<option value="notes">{{ $t('export-targets.all-notes') }}</option>
|
||||
<option value="following">{{ $t('export-targets.following-list') }}</option>
|
||||
<option value="mute">{{ $t('export-targets.mute-list') }}</option>
|
||||
<option value="blocking">{{ $t('export-targets.blocking-list') }}</option>
|
||||
</ui-select>
|
||||
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<details>
|
||||
<summary>{{ $t('danger-zone') }}</summary>
|
||||
<ui-button @click="deleteAccount()">{{ $t('delete-account') }}</ui-button>
|
||||
</details>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { apiUrl, host } from '../../../../config';
|
||||
import { toUnicode } from 'punycode';
|
||||
import langmap from 'langmap';
|
||||
import { unique } from '../../../../../../prelude/array';
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/profile-editor.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
unique,
|
||||
langmap,
|
||||
host: toUnicode(host),
|
||||
enableEmail: false,
|
||||
email: null,
|
||||
name: null,
|
||||
username: null,
|
||||
location: null,
|
||||
description: null,
|
||||
lang: null,
|
||||
birthday: null,
|
||||
avatarId: null,
|
||||
bannerId: null,
|
||||
isCat: false,
|
||||
isBot: false,
|
||||
isLocked: false,
|
||||
carefulBot: false,
|
||||
autoAcceptFollowed: false,
|
||||
saving: false,
|
||||
avatarUploading: false,
|
||||
bannerUploading: false,
|
||||
exportTarget: 'notes',
|
||||
faDownload
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
alwaysMarkNsfw: {
|
||||
get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
|
||||
set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); }
|
||||
},
|
||||
|
||||
bannerStyle(): any {
|
||||
if (this.$store.state.i.bannerUrl == null) return {};
|
||||
return {
|
||||
backgroundColor: this.$store.state.i.bannerColor && this.$store.state.i.bannerColor.length == 3 ? `rgb(${ this.$store.state.i.bannerColor.join(',') })` : null,
|
||||
backgroundImage: `url(${ this.$store.state.i.bannerUrl })`
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.enableEmail = meta.enableEmail;
|
||||
});
|
||||
this.email = this.$store.state.i.email;
|
||||
this.name = this.$store.state.i.name;
|
||||
this.username = this.$store.state.i.username;
|
||||
this.location = this.$store.state.i.profile.location;
|
||||
this.description = this.$store.state.i.description;
|
||||
this.lang = this.$store.state.i.lang;
|
||||
this.birthday = this.$store.state.i.profile.birthday;
|
||||
this.avatarId = this.$store.state.i.avatarId;
|
||||
this.bannerId = this.$store.state.i.bannerId;
|
||||
this.isCat = this.$store.state.i.isCat;
|
||||
this.isBot = this.$store.state.i.isBot;
|
||||
this.isLocked = this.$store.state.i.isLocked;
|
||||
this.carefulBot = this.$store.state.i.carefulBot;
|
||||
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAvatarChange([file]) {
|
||||
this.avatarUploading = true;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('i', this.$store.state.i.token);
|
||||
|
||||
fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
this.avatarId = f.id;
|
||||
this.avatarUploading = false;
|
||||
})
|
||||
.catch(e => {
|
||||
this.avatarUploading = false;
|
||||
alert('%18n:@upload-failed%');
|
||||
});
|
||||
},
|
||||
|
||||
onBannerChange([file]) {
|
||||
this.bannerUploading = true;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('i', this.$store.state.i.token);
|
||||
|
||||
fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
this.bannerId = f.id;
|
||||
this.bannerUploading = false;
|
||||
})
|
||||
.catch(e => {
|
||||
this.bannerUploading = false;
|
||||
alert('%18n:@upload-failed%');
|
||||
});
|
||||
},
|
||||
|
||||
save(notify) {
|
||||
this.saving = true;
|
||||
|
||||
this.$root.api('i/update', {
|
||||
name: this.name || null,
|
||||
location: this.location || null,
|
||||
description: this.description || null,
|
||||
lang: this.lang,
|
||||
birthday: this.birthday || null,
|
||||
avatarId: this.avatarId || undefined,
|
||||
bannerId: this.bannerId || undefined,
|
||||
isCat: !!this.isCat,
|
||||
isBot: !!this.isBot,
|
||||
isLocked: !!this.isLocked,
|
||||
carefulBot: !!this.carefulBot,
|
||||
autoAcceptFollowed: !!this.autoAcceptFollowed
|
||||
}).then(i => {
|
||||
this.saving = false;
|
||||
this.$store.state.i.avatarId = i.avatarId;
|
||||
this.$store.state.i.avatarUrl = i.avatarUrl;
|
||||
this.$store.state.i.bannerId = i.bannerId;
|
||||
this.$store.state.i.bannerUrl = i.bannerUrl;
|
||||
|
||||
if (notify) {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('saved')
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateEmail() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('@.enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/update_email', {
|
||||
password: password,
|
||||
email: this.email == '' ? null : this.email
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
doExport() {
|
||||
this.$root.api(
|
||||
this.exportTarget == 'notes' ? 'i/export-notes' :
|
||||
this.exportTarget == 'following' ? 'i/export-following' :
|
||||
this.exportTarget == 'mute' ? 'i/export-mute' :
|
||||
this.exportTarget == 'blocking' ? 'i/export-blocking' :
|
||||
null, {});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('export-requested')
|
||||
});
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
const { canceled: canceled, result: password } = await this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('i/delete-account', {
|
||||
password
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('account-deleted')
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.esokaraujimuwfttfzgocmutcihewscl
|
||||
> .header
|
||||
height 150px
|
||||
overflow hidden
|
||||
background-size cover
|
||||
background-position center
|
||||
border-radius 4px
|
||||
|
||||
> .avatar
|
||||
position absolute
|
||||
top 0
|
||||
bottom 0
|
||||
left 0
|
||||
right 0
|
||||
display block
|
||||
width 72px
|
||||
height 72px
|
||||
margin auto
|
||||
|
||||
</style>
|
565
src/client/app/common/views/components/settings/settings.vue
Normal file
565
src/client/app/common/views/components/settings/settings.vue
Normal file
@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="nqfhvmnl">
|
||||
<template v-if="page == null || page == 'profile'">
|
||||
<x-profile/>
|
||||
<x-integration/>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'appearance'">
|
||||
<x-theme/>
|
||||
|
||||
<ui-card>
|
||||
<template #title><fa icon="desktop"/> {{ $t('@._settings.appearance') }}</template>
|
||||
|
||||
<section v-if="!$root.isMobile">
|
||||
<ui-switch v-model="showPostFormOnTopOfTl">{{ $t('@._settings.post-form-on-timeline') }}</ui-switch>
|
||||
<ui-button @click="customizeHome">{{ $t('@.customize-home') }}</ui-button>
|
||||
</section>
|
||||
<section v-if="!$root.isMobile">
|
||||
<header>{{ $t('@._settings.wallpaper') }}</header>
|
||||
<ui-horizon-group class="fit-bottom">
|
||||
<ui-button @click="updateWallpaper">{{ $t('@._settings.choose-wallpaper') }}</ui-button>
|
||||
<ui-button @click="deleteWallpaper">{{ $t('@._settings.delete-wallpaper') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
</section>
|
||||
<section v-if="!$root.isMobile">
|
||||
<header>{{ $t('@._settings.navbar-position') }}</header>
|
||||
<ui-radio v-model="navbar" value="top">{{ $t('@._settings.navbar-position-top') }}</ui-radio>
|
||||
<ui-radio v-model="navbar" value="left">{{ $t('@._settings.navbar-position-left') }}</ui-radio>
|
||||
<ui-radio v-model="navbar" value="right">{{ $t('@._settings.navbar-position-right') }}</ui-radio>
|
||||
</section>
|
||||
<section>
|
||||
<ui-switch v-model="darkmode">{{ $t('@.dark-mode') }}</ui-switch>
|
||||
<ui-switch v-model="useShadow">{{ $t('@._settings.use-shadow') }}</ui-switch>
|
||||
<ui-switch v-model="roundedCorners">{{ $t('@._settings.rounded-corners') }}</ui-switch>
|
||||
<ui-switch v-model="circleIcons">{{ $t('@._settings.circle-icons') }}</ui-switch>
|
||||
<ui-switch v-model="reduceMotion">{{ $t('@._settings.reduce-motion') }}</ui-switch>
|
||||
<ui-switch v-model="contrastedAcct">{{ $t('@._settings.contrasted-acct') }}</ui-switch>
|
||||
<ui-switch v-model="showFullAcct">{{ $t('@._settings.show-full-acct') }}</ui-switch>
|
||||
<ui-switch v-model="showVia">{{ $t('@._settings.show-via') }}</ui-switch>
|
||||
<ui-switch v-model="useOsDefaultEmojis">{{ $t('@._settings.use-os-default-emojis') }}</ui-switch>
|
||||
<ui-switch v-model="iLikeSushi">{{ $t('@._settings.i-like-sushi') }}</ui-switch>
|
||||
</section>
|
||||
<section>
|
||||
<ui-switch v-model="suggestRecentHashtags">{{ $t('@._settings.suggest-recent-hashtags') }}</ui-switch>
|
||||
<ui-switch v-model="showClockOnHeader" v-if="!$root.isMobile">{{ $t('@._settings.show-clock-on-header') }}</ui-switch>
|
||||
<ui-switch v-model="alwaysShowNsfw">{{ $t('@._settings.always-show-nsfw') }}</ui-switch>
|
||||
<ui-switch v-model="showReplyTarget">{{ $t('@._settings.show-reply-target') }}</ui-switch>
|
||||
<ui-switch v-model="disableAnimatedMfm">{{ $t('@._settings.disable-animated-mfm') }}</ui-switch>
|
||||
<ui-switch v-model="disableShowingAnimatedImages">{{ $t('@._settings.disable-showing-animated-images') }}</ui-switch>
|
||||
<ui-switch v-model="remainDeletedNote">{{ $t('@._settings.remain-deleted-note') }}</ui-switch>
|
||||
</section>
|
||||
<section>
|
||||
<header>{{ $t('@._settings.line-width') }}</header>
|
||||
<ui-radio v-model="lineWidth" :value="0.5">{{ $t('@._settings.line-width-thin') }}</ui-radio>
|
||||
<ui-radio v-model="lineWidth" :value="1">{{ $t('@._settings.line-width-normal') }}</ui-radio>
|
||||
<ui-radio v-model="lineWidth" :value="2">{{ $t('@._settings.line-width-thick') }}</ui-radio>
|
||||
</section>
|
||||
<section>
|
||||
<header>{{ $t('@._settings.font-size') }}</header>
|
||||
<ui-radio v-model="fontSize" :value="-2">{{ $t('@._settings.font-size-x-small') }}</ui-radio>
|
||||
<ui-radio v-model="fontSize" :value="-1">{{ $t('@._settings.font-size-small') }}</ui-radio>
|
||||
<ui-radio v-model="fontSize" :value="0">{{ $t('@._settings.font-size-medium') }}</ui-radio>
|
||||
<ui-radio v-model="fontSize" :value="1">{{ $t('@._settings.font-size-large') }}</ui-radio>
|
||||
<ui-radio v-model="fontSize" :value="2">{{ $t('@._settings.font-size-x-large') }}</ui-radio>
|
||||
</section>
|
||||
<section v-if="$root.isMobile">
|
||||
<header>{{ $t('@._settings.post-style') }}</header>
|
||||
<ui-radio v-model="postStyle" value="standard">{{ $t('@._settings.post-style-standard') }}</ui-radio>
|
||||
<ui-radio v-model="postStyle" value="smart">{{ $t('@._settings.post-style-smart') }}</ui-radio>
|
||||
</section>
|
||||
<section v-if="$root.isMobile">
|
||||
<header>{{ $t('@._settings.notification-position') }}</header>
|
||||
<ui-radio v-model="mobileNotificationPosition" value="bottom">{{ $t('@._settings.notification-position-bottom') }}</ui-radio>
|
||||
<ui-radio v-model="mobileNotificationPosition" value="top">{{ $t('@._settings.notification-position-top') }}</ui-radio>
|
||||
</section>
|
||||
<section>
|
||||
<header>{{ $t('@._settings.deck-column-align') }}</header>
|
||||
<ui-radio v-model="deckColumnAlign" value="center">{{ $t('@._settings.deck-column-align-center') }}</ui-radio>
|
||||
<ui-radio v-model="deckColumnAlign" value="left">{{ $t('@._settings.deck-column-align-left') }}</ui-radio>
|
||||
<ui-radio v-model="deckColumnAlign" value="flexible">{{ $t('@._settings.deck-column-align-flexible') }}</ui-radio>
|
||||
</section>
|
||||
<section>
|
||||
<header>{{ $t('@._settings.deck-column-width') }}</header>
|
||||
<ui-radio v-model="deckColumnWidth" value="narrow">{{ $t('@._settings.deck-column-width-narrow') }}</ui-radio>
|
||||
<ui-radio v-model="deckColumnWidth" value="narrower">{{ $t('@._settings.deck-column-width-narrower') }}</ui-radio>
|
||||
<ui-radio v-model="deckColumnWidth" value="normal">{{ $t('@._settings.deck-column-width-normal') }}</ui-radio>
|
||||
<ui-radio v-model="deckColumnWidth" value="wider">{{ $t('@._settings.deck-column-width-wider') }}</ui-radio>
|
||||
<ui-radio v-model="deckColumnWidth" value="wide">{{ $t('@._settings.deck-column-width-wide') }}</ui-radio>
|
||||
</section>
|
||||
<section>
|
||||
<ui-switch v-model="games_reversi_showBoardLabels">{{ $t('@._settings.show-reversi-board-labels') }}</ui-switch>
|
||||
<ui-switch v-model="games_reversi_useAvatarStones">{{ $t('@._settings.use-avatar-reversi-stones') }}</ui-switch>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'behavior'">
|
||||
<ui-card>
|
||||
<template #title><fa icon="sliders-h"/> {{ $t('@._settings.behavior') }}</template>
|
||||
|
||||
<section>
|
||||
<ui-switch v-model="fetchOnScroll">{{ $t('@._settings.fetch-on-scroll') }}
|
||||
<template #desc>{{ $t('@._settings.fetch-on-scroll-desc') }}</template>
|
||||
</ui-switch>
|
||||
<ui-switch v-model="keepCw">{{ $t('@._settings.keep-cw') }}
|
||||
<template #desc>{{ $t('@._settings.keep-cw-desc') }}</template>
|
||||
</ui-switch>
|
||||
<ui-switch v-if="$root.isMobile" v-model="disableViaMobile">{{ $t('@._settings.disable-via-mobile') }}</ui-switch>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('@._settings.timeline') }}</header>
|
||||
<ui-switch v-model="showMyRenotes">{{ $t('@._settings.show-my-renotes') }}</ui-switch>
|
||||
<ui-switch v-model="showRenotedMyNotes">{{ $t('@._settings.show-renoted-my-notes') }}</ui-switch>
|
||||
<ui-switch v-model="showLocalRenotes">{{ $t('@._settings.show-local-renotes') }}</ui-switch>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('@._settings.note-visibility') }}</header>
|
||||
<ui-switch v-model="rememberNoteVisibility">{{ $t('@._settings.remember-note-visibility') }}</ui-switch>
|
||||
<section>
|
||||
<header>{{ $t('@._settings.default-note-visibility') }}</header>
|
||||
<ui-select v-model="defaultNoteVisibility">
|
||||
<option value="public">{{ $t('@.note-visibility.public') }}</option>
|
||||
<option value="home">{{ $t('@.note-visibility.home') }}</option>
|
||||
<option value="followers">{{ $t('@.note-visibility.followers') }}</option>
|
||||
<option value="specified">{{ $t('@.note-visibility.specified') }}</option>
|
||||
<option value="local-public">{{ $t('@.note-visibility.local-public') }}</option>
|
||||
<option value="local-home">{{ $t('@.note-visibility.local-home') }}</option>
|
||||
<option value="local-followers">{{ $t('@.note-visibility.local-followers') }}</option>
|
||||
</ui-select>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<header>{{ $t('@._settings.web-search-engine') }}</header>
|
||||
<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template></ui-input>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<template #title><fa icon="volume-up"/> {{ $t('@._settings.sound') }}</template>
|
||||
|
||||
<section>
|
||||
<ui-switch v-model="enableSounds">{{ $t('@._settings.enable-sounds') }}
|
||||
<template #desc>{{ $t('@._settings.enable-sounds-desc') }}</template>
|
||||
</ui-switch>
|
||||
<label>{{ $t('@._settings.volume') }}</label>
|
||||
<input type="range"
|
||||
v-model="soundVolume"
|
||||
:disabled="!enableSounds"
|
||||
max="1"
|
||||
step="0.1"
|
||||
/>
|
||||
<ui-button @click="soundTest"><fa icon="volume-up"/> {{ $t('@._settings.test') }}</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<x-language/>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'notification'">
|
||||
<x-notification v-show="page == 'notification'"/>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'drive'">
|
||||
<x-drive/>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'hashtags'">
|
||||
<ui-card>
|
||||
<template #title><fa icon="hashtag"/> {{ $t('@._settings.tags') }}</template>
|
||||
<section>
|
||||
<x-tags/>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'muteAndBlock'">
|
||||
<x-mute-and-block/>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'apps'">
|
||||
<ui-card>
|
||||
<template #title><fa icon="puzzle-piece"/> {{ $t('@._settings.apps') }}</template>
|
||||
<section>
|
||||
<x-apps/>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'security'">
|
||||
<ui-card>
|
||||
<template #title><fa icon="unlock-alt"/> {{ $t('@._settings.password') }}</template>
|
||||
<section>
|
||||
<x-password/>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card v-if="!$root.isMobile">
|
||||
<template #title><fa icon="mobile-alt"/> {{ $t('@.2fa') }}</template>
|
||||
<section>
|
||||
<x-2fa/>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<template #title><fa icon="sign-in-alt"/> {{ $t('@._settings.signin') }}</template>
|
||||
<section>
|
||||
<x-signins/>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'api'">
|
||||
<x-api/>
|
||||
</template>
|
||||
|
||||
<template v-if="page == null || page == 'other'">
|
||||
<ui-card>
|
||||
<template #title><fa icon="sync-alt"/> {{ $t('@._settings.update') }}</template>
|
||||
<section>
|
||||
<p>
|
||||
<span>{{ $t('@._settings.version') }} <i>{{ version }}</i></span>
|
||||
<template v-if="latestVersion !== undefined">
|
||||
<br>
|
||||
<span>{{ $t('@._settings.latest-version') }} <i>{{ latestVersion ? latestVersion : version }}</i></span>
|
||||
</template>
|
||||
</p>
|
||||
<ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
|
||||
<template v-if="checkingForUpdate">{{ $t('@._settings.update-checking') }}<mk-ellipsis/></template>
|
||||
<template v-else>{{ $t('@._settings.do-update') }}</template>
|
||||
</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<template #title><fa icon="cogs"/> {{ $t('@._settings.advanced-settings') }}</template>
|
||||
<section>
|
||||
<ui-switch v-model="debug">
|
||||
{{ $t('@._settings.debug-mode') }}<template #desc>{{ $t('@._settings.debug-mode-desc') }}</template>
|
||||
</ui-switch>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import X2fa from './2fa.vue';
|
||||
import XApps from './apps.vue';
|
||||
import XSignins from './signins.vue';
|
||||
import XTags from './tags.vue';
|
||||
import XIntegration from './integration.vue';
|
||||
import XTheme from './theme.vue';
|
||||
import XDrive from './drive.vue';
|
||||
import XMuteAndBlock from './mute-and-block.vue';
|
||||
import XPassword from './password.vue';
|
||||
import XProfile from './profile.vue';
|
||||
import XApi from './api.vue';
|
||||
import XLanguage from './language.vue';
|
||||
import XNotification from './notification.vue';
|
||||
|
||||
import { url, version } from '../../../../config';
|
||||
import checkForUpdate from '../../../scripts/check-for-update';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
components: {
|
||||
X2fa,
|
||||
XApps,
|
||||
XSignins,
|
||||
XTags,
|
||||
XIntegration,
|
||||
XTheme,
|
||||
XDrive,
|
||||
XMuteAndBlock,
|
||||
XPassword,
|
||||
XProfile,
|
||||
XApi,
|
||||
XLanguage,
|
||||
XNotification,
|
||||
},
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
meta: null,
|
||||
version,
|
||||
latestVersion: undefined,
|
||||
checkingForUpdate: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
useOsDefaultEmojis: {
|
||||
get() { return this.$store.state.device.useOsDefaultEmojis; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); }
|
||||
},
|
||||
|
||||
reduceMotion: {
|
||||
get() { return this.$store.state.device.reduceMotion; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
|
||||
},
|
||||
|
||||
keepCw: {
|
||||
get() { return this.$store.state.settings.keepCw; },
|
||||
set(value) { this.$store.commit('settings/set', { key: 'keepCw', value }); }
|
||||
},
|
||||
|
||||
darkmode: {
|
||||
get() { return this.$store.state.device.darkmode; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
|
||||
},
|
||||
|
||||
navbar: {
|
||||
get() { return this.$store.state.device.navbar; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'navbar', value }); }
|
||||
},
|
||||
|
||||
deckColumnAlign: {
|
||||
get() { return this.$store.state.device.deckColumnAlign; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
|
||||
},
|
||||
|
||||
deckColumnWidth: {
|
||||
get() { return this.$store.state.device.deckColumnWidth; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'deckColumnWidth', value }); }
|
||||
},
|
||||
|
||||
enableSounds: {
|
||||
get() { return this.$store.state.device.enableSounds; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
|
||||
},
|
||||
|
||||
soundVolume: {
|
||||
get() { return this.$store.state.device.soundVolume; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'soundVolume', value }); }
|
||||
},
|
||||
|
||||
debug: {
|
||||
get() { return this.$store.state.device.debug; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'debug', value }); }
|
||||
},
|
||||
|
||||
alwaysShowNsfw: {
|
||||
get() { return this.$store.state.device.alwaysShowNsfw; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
|
||||
},
|
||||
|
||||
postStyle: {
|
||||
get() { return this.$store.state.device.postStyle; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
|
||||
},
|
||||
|
||||
disableViaMobile: {
|
||||
get() { return this.$store.state.settings.disableViaMobile; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
|
||||
},
|
||||
|
||||
useShadow: {
|
||||
get() { return this.$store.state.device.useShadow; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'useShadow', value }); }
|
||||
},
|
||||
|
||||
roundedCorners: {
|
||||
get() { return this.$store.state.device.roundedCorners; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'roundedCorners', value }); }
|
||||
},
|
||||
|
||||
lineWidth: {
|
||||
get() { return this.$store.state.device.lineWidth; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'lineWidth', value }); }
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
get() { return this.$store.state.device.fontSize; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'fontSize', value }); }
|
||||
},
|
||||
|
||||
fetchOnScroll: {
|
||||
get() { return this.$store.state.settings.fetchOnScroll; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
|
||||
},
|
||||
|
||||
rememberNoteVisibility: {
|
||||
get() { return this.$store.state.settings.rememberNoteVisibility; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
|
||||
},
|
||||
|
||||
defaultNoteVisibility: {
|
||||
get() { return this.$store.state.settings.defaultNoteVisibility; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
|
||||
},
|
||||
|
||||
webSearchEngine: {
|
||||
get() { return this.$store.state.settings.webSearchEngine; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'webSearchEngine', value }); }
|
||||
},
|
||||
|
||||
showReplyTarget: {
|
||||
get() { return this.$store.state.settings.showReplyTarget; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
|
||||
},
|
||||
|
||||
showMyRenotes: {
|
||||
get() { return this.$store.state.settings.showMyRenotes; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
|
||||
},
|
||||
|
||||
showRenotedMyNotes: {
|
||||
get() { return this.$store.state.settings.showRenotedMyNotes; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
|
||||
},
|
||||
|
||||
showLocalRenotes: {
|
||||
get() { return this.$store.state.settings.showLocalRenotes; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
|
||||
},
|
||||
|
||||
showPostFormOnTopOfTl: {
|
||||
get() { return this.$store.state.settings.showPostFormOnTopOfTl; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); }
|
||||
},
|
||||
|
||||
suggestRecentHashtags: {
|
||||
get() { return this.$store.state.settings.suggestRecentHashtags; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); }
|
||||
},
|
||||
|
||||
showClockOnHeader: {
|
||||
get() { return this.$store.state.settings.showClockOnHeader; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); }
|
||||
},
|
||||
|
||||
circleIcons: {
|
||||
get() { return this.$store.state.settings.circleIcons; },
|
||||
set(value) {
|
||||
this.$store.dispatch('settings/set', { key: 'circleIcons', value });
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
|
||||
contrastedAcct: {
|
||||
get() { return this.$store.state.settings.contrastedAcct; },
|
||||
set(value) {
|
||||
this.$store.dispatch('settings/set', { key: 'contrastedAcct', value });
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
|
||||
showFullAcct: {
|
||||
get() { return this.$store.state.settings.showFullAcct; },
|
||||
set(value) {
|
||||
this.$store.dispatch('settings/set', { key: 'showFullAcct', value });
|
||||
this.reload();
|
||||
}
|
||||
},
|
||||
|
||||
showVia: {
|
||||
get() { return this.$store.state.settings.showVia; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'showVia', value }); }
|
||||
},
|
||||
|
||||
iLikeSushi: {
|
||||
get() { return this.$store.state.settings.iLikeSushi; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
|
||||
},
|
||||
|
||||
games_reversi_showBoardLabels: {
|
||||
get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
|
||||
},
|
||||
|
||||
games_reversi_useAvatarStones: {
|
||||
get() { return this.$store.state.settings.games.reversi.useAvatarStones; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useAvatarStones', value }); }
|
||||
},
|
||||
|
||||
disableAnimatedMfm: {
|
||||
get() { return this.$store.state.settings.disableAnimatedMfm; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
|
||||
},
|
||||
|
||||
disableShowingAnimatedImages: {
|
||||
get() { return this.$store.state.device.disableShowingAnimatedImages; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); }
|
||||
},
|
||||
|
||||
remainDeletedNote: {
|
||||
get() { return this.$store.state.settings.remainDeletedNote; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'remainDeletedNote', value }); }
|
||||
},
|
||||
|
||||
mobileNotificationPosition: {
|
||||
get() { return this.$store.state.device.mobileNotificationPosition; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); }
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.meta = meta;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('@.reload-to-apply-the-setting'),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (!canceled) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
customizeHome() {
|
||||
location.href = '/?customize';
|
||||
},
|
||||
updateWallpaper() {
|
||||
this.$chooseDriveFile({
|
||||
multiple: false
|
||||
}).then(file => {
|
||||
this.$root.api('i/update', {
|
||||
wallpaperId: file.id
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteWallpaper() {
|
||||
this.$root.api('i/update', {
|
||||
wallpaperId: null
|
||||
});
|
||||
},
|
||||
checkForUpdate() {
|
||||
this.checkingForUpdate = true;
|
||||
checkForUpdate(this.$root, true, true).then(newer => {
|
||||
this.checkingForUpdate = false;
|
||||
this.latestVersion = newer;
|
||||
if (newer == null) {
|
||||
this.$root.dialog({
|
||||
title: this.$t('no-updates'),
|
||||
text: this.$t('no-updates-desc')
|
||||
});
|
||||
} else {
|
||||
this.$root.dialog({
|
||||
title: this.$t('update-available'),
|
||||
text: this.$t('update-available-desc')
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
soundTest() {
|
||||
const sound = new Audio(`${url}/assets/message.mp3`);
|
||||
sound.volume = this.$store.state.device.soundVolume;
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
98
src/client/app/common/views/components/settings/signins.vue
Normal file
98
src/client/app/common/views/components/settings/signins.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="root">
|
||||
<div class="signins" v-if="signins.length != 0">
|
||||
<div v-for="signin in signins">
|
||||
<header @click="signin._show = !signin._show">
|
||||
<template v-if="signin.success"><fa icon="check"/></template>
|
||||
<template v-else><fa icon="times"/></template>
|
||||
<span class="ip">{{ signin.ip }}</span>
|
||||
<mk-time :time="signin.createdAt"/>
|
||||
</header>
|
||||
<div class="headers" v-show="signin._show">
|
||||
<!-- TODO -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
signins: [],
|
||||
connection: null
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$root.api('i/signin_history').then(signins => {
|
||||
this.signins = signins;
|
||||
this.fetching = false;
|
||||
});
|
||||
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
|
||||
this.connection.on('signin', this.onSignin);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSignin(signin) {
|
||||
this.signins.unshift(signin);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.root
|
||||
> .signins
|
||||
> div
|
||||
border-bottom solid 1px #eee
|
||||
|
||||
> header
|
||||
display flex
|
||||
padding 8px 0
|
||||
line-height 32px
|
||||
cursor pointer
|
||||
|
||||
> [data-icon]
|
||||
margin-right 8px
|
||||
text-align left
|
||||
|
||||
&.check
|
||||
color #0fda82
|
||||
|
||||
&.times
|
||||
color #ff3100
|
||||
|
||||
> .ip
|
||||
display inline-block
|
||||
text-align left
|
||||
padding 8px
|
||||
line-height 16px
|
||||
font-family monospace
|
||||
font-size 14px
|
||||
color #444
|
||||
background #f8f8f8
|
||||
border-radius 4px
|
||||
|
||||
> .mk-time
|
||||
margin-left auto
|
||||
text-align right
|
||||
color #777
|
||||
|
||||
> .headers
|
||||
overflow auto
|
||||
margin 0 0 16px 0
|
||||
max-height 100px
|
||||
white-space pre-wrap
|
||||
word-break break-all
|
||||
|
||||
</style>
|
67
src/client/app/common/views/components/settings/tags.vue
Normal file
67
src/client/app/common/views/components/settings/tags.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="vfcitkilproprqtbnpoertpsziierwzi">
|
||||
<div v-for="timeline in timelines" class="timeline" :key="timeline.id">
|
||||
<ui-input v-model="timeline.title" @change="save">
|
||||
<span>{{ $t('title') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" :pre="true" @input="onQueryChange(timeline, $event)">
|
||||
<span>{{ $t('query') }}</span>
|
||||
</ui-textarea>
|
||||
</div>
|
||||
<ui-button class="add" @click="add">{{ $t('add') }}</ui-button>
|
||||
<ui-button class="save" @click="save">{{ $t('save') }}</ui-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/settings.tags.vue'),
|
||||
data() {
|
||||
return {
|
||||
timelines: this.$store.state.settings.tagTimelines
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
add() {
|
||||
this.timelines.push({
|
||||
id: uuid(),
|
||||
title: '',
|
||||
query: ''
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
const timelines = this.timelines
|
||||
.filter(timeline => timeline.title)
|
||||
.map(timeline => {
|
||||
if (!(timeline.query && timeline.query[0] && timeline.query[0][0])) {
|
||||
timeline.query = timeline.title.split('\n').map(tags => tags.split(' '));
|
||||
}
|
||||
return timeline;
|
||||
});
|
||||
|
||||
this.$store.dispatch('settings/set', { key: 'tagTimelines', value: timelines });
|
||||
},
|
||||
|
||||
onQueryChange(timeline, value) {
|
||||
timeline.query = value.split('\n').map(tags => tags.split(' '));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.vfcitkilproprqtbnpoertpsziierwzi
|
||||
> .timeline
|
||||
padding-bottom 16px
|
||||
border-bottom solid 1px rgba(#000, 0.1)
|
||||
|
||||
> .add
|
||||
margin-top 16px
|
||||
|
||||
</style>
|
357
src/client/app/common/views/components/settings/theme.vue
Normal file
357
src/client/app/common/views/components/settings/theme.vue
Normal file
@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="palette"/> {{ $t('theme') }}</template>
|
||||
<section class="nicnklzforebnpfgasiypmpdaaglujqm fit-top">
|
||||
<label>
|
||||
<ui-select v-model="light" :placeholder="$t('light-theme')">
|
||||
<template #label><fa :icon="faSun"/> {{ $t('light-theme') }}</template>
|
||||
<optgroup :label="$t('light-themes')">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('dark-themes')">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<ui-select v-model="dark" :placeholder="$t('dark-theme')">
|
||||
<template #label><fa :icon="faMoon"/> {{ $t('dark-theme') }}</template>
|
||||
<optgroup :label="$t('dark-themes')">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('light-themes')">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
</label>
|
||||
|
||||
<a href="https://assets.msky.cafe/theme/list" target="_blank">{{ $t('find-more-theme') }}</a>
|
||||
|
||||
<details class="creator">
|
||||
<summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary>
|
||||
<div>
|
||||
<span>{{ $t('base-theme') }}:</span>
|
||||
<ui-radio v-model="myThemeBase" value="light">{{ $t('base-theme-light') }}</ui-radio>
|
||||
<ui-radio v-model="myThemeBase" value="dark">{{ $t('base-theme-dark') }}</ui-radio>
|
||||
</div>
|
||||
<div>
|
||||
<ui-input v-model="myThemeName">
|
||||
<span>{{ $t('theme-name') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-model="myThemeDesc">
|
||||
<span>{{ $t('desc') }}</span>
|
||||
</ui-textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom:8px;">{{ $t('primary-color') }}:</div>
|
||||
<color-picker v-model="myThemePrimary"/>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom:8px;">{{ $t('secondary-color') }}:</div>
|
||||
<color-picker v-model="myThemeSecondary"/>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom:8px;">{{ $t('text-color') }}:</div>
|
||||
<color-picker v-model="myThemeText"/>
|
||||
</div>
|
||||
<ui-button @click="preview()"><fa icon="eye"/> {{ $t('preview-created-theme') }}</ui-button>
|
||||
<ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> {{ $t('save-created-theme') }}</ui-button>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><fa icon="download"/> {{ $t('install-a-theme') }}</summary>
|
||||
<ui-button @click="import_()"><fa icon="file-import"/> {{ $t('import') }}</ui-button>
|
||||
<input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
|
||||
<p>{{ $t('import-by-code') }}:</p>
|
||||
<ui-textarea v-model="installThemeCode">
|
||||
<span>{{ $t('theme-code') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> {{ $t('install') }}</ui-button>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary>
|
||||
<ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')">
|
||||
<optgroup :label="$t('builtin-themes')">
|
||||
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('my-themes')">
|
||||
<option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('installed-themes')">
|
||||
<option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
<template v-if="selectedTheme">
|
||||
<ui-input readonly :value="selectedTheme.author">
|
||||
<span>{{ $t('author') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc">
|
||||
<span>{{ $t('desc') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-textarea readonly tall :value="selectedThemeCode">
|
||||
<span>{{ $t('theme-code') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button>
|
||||
<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button>
|
||||
</template>
|
||||
</details>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme';
|
||||
import { Chrome } from 'vue-color';
|
||||
import * as uuid from 'uuid';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import * as JSON5 from 'json5';
|
||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
// 後方互換性のため
|
||||
function convertOldThemedefinition(t) {
|
||||
const t2 = {
|
||||
id: t.meta.id,
|
||||
name: t.meta.name,
|
||||
author: t.meta.author,
|
||||
base: t.meta.base,
|
||||
vars: t.meta.vars,
|
||||
props: t
|
||||
};
|
||||
delete t2.props.meta;
|
||||
return t2;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/theme.vue'),
|
||||
components: {
|
||||
ColorPicker: Chrome
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
builtinThemes: builtinThemes,
|
||||
installThemeCode: null,
|
||||
selectedThemeId: null,
|
||||
myThemeBase: 'light',
|
||||
myThemeName: '',
|
||||
myThemeDesc: '',
|
||||
myThemePrimary: lightTheme.vars.primary,
|
||||
myThemeSecondary: lightTheme.vars.secondary,
|
||||
myThemeText: lightTheme.vars.text,
|
||||
faMoon, faSun
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
themes(): Theme[] {
|
||||
return builtinThemes.concat(this.$store.state.device.themes);
|
||||
},
|
||||
|
||||
darkThemes(): Theme[] {
|
||||
return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
|
||||
},
|
||||
|
||||
lightThemes(): Theme[] {
|
||||
return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
|
||||
},
|
||||
|
||||
installedThemes(): Theme[] {
|
||||
return this.$store.state.device.themes;
|
||||
},
|
||||
|
||||
light: {
|
||||
get() { return this.$store.state.device.lightTheme; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
|
||||
},
|
||||
|
||||
dark: {
|
||||
get() { return this.$store.state.device.darkTheme; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
|
||||
},
|
||||
|
||||
selectedTheme() {
|
||||
if (this.selectedThemeId == null) return null;
|
||||
return this.themes.find(x => x.id == this.selectedThemeId);
|
||||
},
|
||||
|
||||
selectedThemeCode() {
|
||||
if (this.selectedTheme == null) return null;
|
||||
return JSON5.stringify(this.selectedTheme, null, '\t');
|
||||
},
|
||||
|
||||
myTheme(): any {
|
||||
return {
|
||||
name: this.myThemeName,
|
||||
author: this.$store.state.i.username,
|
||||
desc: this.myThemeDesc,
|
||||
base: this.myThemeBase,
|
||||
vars: {
|
||||
primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
|
||||
secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
|
||||
text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
myThemeBase(v) {
|
||||
const theme = v == 'light' ? lightTheme : darkTheme;
|
||||
this.myThemePrimary = theme.vars.primary;
|
||||
this.myThemeSecondary = theme.vars.secondary;
|
||||
this.myThemeText = theme.vars.text;
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate() {
|
||||
// migrate old theme definitions
|
||||
// 後方互換性のため
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: this.$store.state.device.themes.map(t => {
|
||||
if (t.id == null) {
|
||||
return convertOldThemedefinition(t);
|
||||
} else {
|
||||
return t;
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
install(code) {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (e) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('invalid-theme')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 後方互換性のため
|
||||
if (theme.id == null && theme.meta != null) {
|
||||
theme = convertOldThemedefinition(theme);
|
||||
}
|
||||
|
||||
if (theme.id == null) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('invalid-theme')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('already-installed')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const themes = this.$store.state.device.themes.concat(theme);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('installed').replace('{}', theme.name)
|
||||
});
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
const theme = this.selectedTheme;
|
||||
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('uninstalled').replace('{}', theme.name)
|
||||
});
|
||||
},
|
||||
|
||||
import_() {
|
||||
(this.$refs.file as any).click();
|
||||
}
|
||||
|
||||
export_() {
|
||||
const blob = new Blob([this.selectedThemeCode], {
|
||||
type: 'application/json5'
|
||||
});
|
||||
this.$refs.export.$el.href = window.URL.createObjectURL(blob);
|
||||
},
|
||||
|
||||
onUpdateImportFile() {
|
||||
const f = (this.$refs.file as any).files[0];
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = e => {
|
||||
this.install(e.target.result);
|
||||
};
|
||||
|
||||
reader.readAsText(f);
|
||||
},
|
||||
|
||||
preview() {
|
||||
applyTheme(this.myTheme, false);
|
||||
},
|
||||
|
||||
gen() {
|
||||
const theme = this.myTheme;
|
||||
|
||||
if (theme.name == null || theme.name.trim() == '') {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('theme-name-required')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
theme.id = uuid();
|
||||
|
||||
const themes = this.$store.state.device.themes.concat(theme);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('saved')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.nicnklzforebnpfgasiypmpdaaglujqm
|
||||
> a
|
||||
display block
|
||||
margin-top -16px
|
||||
margin-bottom 16px
|
||||
|
||||
> details
|
||||
border-top solid var(--lineWidth) var(--faceDivider)
|
||||
|
||||
> summary
|
||||
padding 16px 0
|
||||
|
||||
> *:last-child
|
||||
margin-bottom 16px
|
||||
|
||||
> .creator
|
||||
> div
|
||||
padding 16px 0
|
||||
border-bottom solid var(--lineWidth) var(--faceDivider)
|
||||
</style>
|
Reference in New Issue
Block a user