クライアントの設定コンポーネントを整理

* デスクトップとモバイルで統一
* いくつかの設定を廃止
This commit is contained in:
syuilo
2019-03-01 10:42:28 +09:00
parent d83efecc94
commit ea1818284b
26 changed files with 737 additions and 1206 deletions

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

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

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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