Merge remote-tracking branch 'misskey-dev/master' into develop

This commit is contained in:
2022-06-11 20:36:00 +09:00
544 changed files with 14952 additions and 12562 deletions

View File

@ -150,6 +150,7 @@ const patrons = [
'Weeble',
'蝉暮せせせ',
'ThatOneCalculator',
'pixeldesu',
];
let easterEggReady = false;

View File

@ -24,10 +24,10 @@
</div>
<!-- TODO
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
<span>{{ $ts.username }}</span>
</MkInput>
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'">
<span>{{ $ts.host }}</span>
</MkInput>
</div>
@ -41,8 +41,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { computed } from 'vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@ -50,45 +50,35 @@ import MkPagination from '@/components/ui/pagination.vue';
import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkInput,
MkSelect,
MkPagination,
XAbuseReport,
},
let reports = $ref<InstanceType<typeof MkPagination>>();
emits: ['info'],
let state = $ref('unresolved');
let reporterOrigin = $ref('combined');
let targetUserOrigin = $ref('combined');
let searchUsername = $ref('');
let searchHost = $ref('');
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
},
searchUsername: '',
searchHost: '',
state: 'unresolved',
reporterOrigin: 'combined',
targetUserOrigin: 'combined',
pagination: {
endpoint: 'admin/abuse-user-reports' as const,
limit: 10,
params: computed(() => ({
state: this.state,
reporterOrigin: this.reporterOrigin,
targetUserOrigin: this.targetUserOrigin,
})),
},
}
},
const pagination = {
endpoint: 'admin/abuse-user-reports' as const,
limit: 10,
params: computed(() => ({
state,
reporterOrigin,
targetUserOrigin,
})),
};
methods: {
resolved(reportId) {
this.$refs.reports.removeItem(item => item.id === reportId);
},
function resolved(reportId) {
reports.removeItem(item => item.id === reportId);
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
}
});
</script>

View File

@ -7,7 +7,7 @@
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ $ts.imageUrl }}</template>
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
@ -17,34 +17,34 @@
</FormRadios>
<!--
<div style="margin: 32px 0;">
{{ $ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
{{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ $ts.ratio }}</template>
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ $ts.expiration }}</template>
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ $ts.memo }}</template>
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons _formBlock">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
@ -52,81 +52,65 @@ import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
FormRadios,
FormSplit,
},
let ads: any[] = $ref([]);
emits: ['info'],
os.api('admin/ad/list').then(adsResponse => {
ads = adsResponse;
});
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.add,
handler: this.add,
}],
},
ads: [],
}
},
function add() {
ads.unshift({
id: null,
memo: '',
place: 'square',
priority: 'middle',
ratio: 1,
url: '',
imageUrl: null,
expiresAt: null,
});
}
created() {
os.api('admin/ad/list').then(ads => {
this.ads = ads;
function remove(ad) {
os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
ads = ads.filter(x => x !== ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id
});
},
});
}
methods: {
add() {
this.ads.unshift({
id: null,
memo: '',
place: 'square',
priority: 'middle',
ratio: 1,
url: '',
imageUrl: null,
expiresAt: null,
});
},
function save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
}
}
remove(ad) {
os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
this.ads = this.ads.filter(x => x != ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id
});
});
},
save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}],
}
});
</script>

View File

@ -3,112 +3,98 @@
<section v-for="announcement in announcements" class="_card _gap announcements">
<div class="_content announcement">
<MkInput v-model="announcement.title">
<template #label>{{ $ts.title }}</template>
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="announcement.text">
<template #label>{{ $ts.text }}</template>
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkInput v-model="announcement.imageUrl">
<template #label>{{ $ts.imageUrl }}</template>
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
<div class="buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
},
let announcements: any[] = $ref([]);
emits: ['info'],
os.api('admin/announcements/list').then(announcementResponse => {
announcements = announcementResponse;
});
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.add,
handler: this.add,
}],
},
announcements: [],
}
},
function add() {
announcements.unshift({
id: null,
title: '',
text: '',
imageUrl: null
});
}
created() {
os.api('admin/announcements/list').then(announcements => {
this.announcements = announcements;
function remove(announcement) {
os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: announcement.title }),
}).then(({ canceled }) => {
if (canceled) return;
announcements = announcements.filter(x => x !== announcement);
os.api('admin/announcements/delete', announcement);
});
}
function save(announcement) {
if (announcement.id == null) {
os.api('admin/announcements/create', announcement).then(() => {
os.alert({
type: 'success',
text: i18n.ts.saved
});
}).catch(err => {
os.alert({
type: 'error',
text: err
});
});
},
methods: {
add() {
this.announcements.unshift({
id: null,
title: '',
text: '',
imageUrl: null
} else {
os.api('admin/announcements/update', announcement).then(() => {
os.alert({
type: 'success',
text: i18n.ts.saved
});
},
remove(announcement) {
os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: announcement.title }),
}).then(({ canceled }) => {
if (canceled) return;
this.announcements = this.announcements.filter(x => x != announcement);
os.api('admin/announcements/delete', announcement);
}).catch(err => {
os.alert({
type: 'error',
text: err
});
},
});
}
}
save(announcement) {
if (announcement.id == null) {
os.api('admin/announcements/create', announcement).then(() => {
os.alert({
type: 'success',
text: this.$ts.saved
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
} else {
os.api('admin/announcements/update', announcement).then(() => {
os.alert({
type: 'success',
text: this.$ts.saved
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}],
}
});
</script>

View File

@ -43,8 +43,8 @@
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import FormRadios from '@/components/form/radios.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
@ -54,64 +54,39 @@ import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormRadios,
FormInput,
FormButton,
FormSuspense,
FormSlot,
MkCaptcha: defineAsyncComponent(() => import('@/components/captcha.vue')),
},
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));
emits: ['info'],
let provider = $ref(null);
let hcaptchaSiteKey: string | null = $ref(null);
let hcaptchaSecretKey: string | null = $ref(null);
let recaptchaSiteKey: string | null = $ref(null);
let recaptchaSecretKey: string | null = $ref(null);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.botProtection,
icon: 'fas fa-shield-alt'
},
provider: null,
enableHcaptcha: false,
hcaptchaSiteKey: null,
hcaptchaSecretKey: null,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
}
},
const enableHcaptcha = $computed(() => provider === 'hcaptcha');
const enableRecaptcha = $computed(() => provider === 'recaptcha');
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableHcaptcha = meta.enableHcaptcha;
this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
this.enableRecaptcha = meta.enableRecaptcha;
this.recaptchaSiteKey = meta.recaptchaSiteKey;
this.recaptchaSecretKey = meta.recaptchaSecretKey;
async function init() {
const meta = await os.api('admin/meta');
enableHcaptcha = meta.enableHcaptcha;
hcaptchaSiteKey = meta.hcaptchaSiteKey;
hcaptchaSecretKey = meta.hcaptchaSecretKey;
enableRecaptcha = meta.enableRecaptcha;
recaptchaSiteKey = meta.recaptchaSiteKey;
recaptchaSecretKey = meta.recaptchaSecretKey;
this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null;
}
this.$watch(() => this.provider, () => {
this.enableHcaptcha = this.provider === 'hcaptcha';
this.enableRecaptcha = this.provider === 'recaptcha';
});
},
save() {
os.apiWithDialog('admin/update-meta', {
enableHcaptcha: this.enableHcaptcha,
hcaptchaSiteKey: this.hcaptchaSiteKey,
hcaptchaSecretKey: this.hcaptchaSecretKey,
enableRecaptcha: this.enableRecaptcha,
recaptchaSiteKey: this.recaptchaSiteKey,
recaptchaSecretKey: this.recaptchaSecretKey,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableHcaptcha,
hcaptchaSiteKey,
hcaptchaSecretKey,
enableRecaptcha,
recaptchaSiteKey,
recaptchaSecretKey,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@ -9,36 +9,23 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSuspense,
MkKeyValue,
},
const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
},
databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
}
},
methods: {
bytes, number,
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
}
});
</script>

View File

@ -3,37 +3,37 @@
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="enableEmail" class="_formBlock">
<template #label>{{ $ts.enableEmail }}</template>
<template #caption>{{ $ts.emailConfigInfo }}</template>
<template #label>{{ i18n.ts.enableEmail }}</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</FormSwitch>
<template v-if="enableEmail">
<FormInput v-model="email" type="email" class="_formBlock">
<template #label>{{ $ts.emailAddress }}</template>
<template #label>{{ i18n.ts.emailAddress }}</template>
</FormInput>
<FormSection>
<template #label>{{ $ts.smtpConfig }}</template>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<FormSplit :min-width="280">
<FormInput v-model="smtpHost" class="_formBlock">
<template #label>{{ $ts.smtpHost }}</template>
<template #label>{{ i18n.ts.smtpHost }}</template>
</FormInput>
<FormInput v-model="smtpPort" type="number" class="_formBlock">
<template #label>{{ $ts.smtpPort }}</template>
<template #label>{{ i18n.ts.smtpPort }}</template>
</FormInput>
</FormSplit>
<FormSplit :min-width="280">
<FormInput v-model="smtpUser" class="_formBlock">
<template #label>{{ $ts.smtpUser }}</template>
<template #label>{{ i18n.ts.smtpUser }}</template>
</FormInput>
<FormInput v-model="smtpPass" type="password" class="_formBlock">
<template #label>{{ $ts.smtpPass }}</template>
<template #label>{{ i18n.ts.smtpPass }}</template>
</FormInput>
</FormSplit>
<FormInfo class="_formBlock">{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormSwitch v-model="smtpSecure" class="_formBlock">
<template #label>{{ $ts.smtpSecure }}</template>
<template #caption>{{ $ts.smtpSecureInfo }}</template>
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</FormSwitch>
</FormSection>
</template>
@ -42,8 +42,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormInfo from '@/components/ui/info.vue';
@ -52,86 +52,71 @@ import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { fetchInstance, instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSplit,
FormSection,
FormInfo,
FormSuspense,
},
let enableEmail: boolean = $ref(false);
let email: any = $ref(null);
let smtpSecure: boolean = $ref(false);
let smtpHost: string = $ref('');
let smtpPort: number = $ref(0);
let smtpUser: string = $ref('');
let smtpPass: string = $ref('');
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
enableEmail = meta.enableEmail;
email = meta.email;
smtpSecure = meta.smtpSecure;
smtpHost = meta.smtpHost;
smtpPort = meta.smtpPort;
smtpUser = meta.smtpUser;
smtpPass = meta.smtpPass;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
text: this.$ts.testEmail,
handler: this.testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
enableEmail: false,
email: null,
smtpSecure: false,
smtpHost: '',
smtpPort: 0,
smtpUser: '',
smtpPass: '',
}
},
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination,
type: 'email',
placeholder: instance.maintainerEmail
});
if (canceled) return;
os.apiWithDialog('admin/send-email', {
to: destination,
subject: 'Test email',
text: 'Yo'
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableEmail = meta.enableEmail;
this.email = meta.email;
this.smtpSecure = meta.smtpSecure;
this.smtpHost = meta.smtpHost;
this.smtpPort = meta.smtpPort;
this.smtpUser = meta.smtpUser;
this.smtpPass = meta.smtpPass;
},
function save() {
os.apiWithDialog('admin/update-meta', {
enableEmail,
email,
smtpSecure,
smtpHost,
smtpPort,
smtpUser,
smtpPass,
}).then(() => {
fetchInstance();
});
}
async testEmail() {
const { canceled, result: destination } = await os.inputText({
title: this.$ts.destination,
type: 'email',
placeholder: this.$instance.maintainerEmail
});
if (canceled) return;
os.apiWithDialog('admin/send-email', {
to: destination,
subject: 'Test email',
text: 'Yo'
});
},
save() {
os.apiWithDialog('admin/update-meta', {
enableEmail: this.enableEmail,
email: this.email,
smtpSecure: this.smtpSecure,
smtpHost: this.smtpHost,
smtpPort: this.smtpPort,
smtpUser: this.smtpUser,
smtpPass: this.smtpPass,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
text: i18n.ts.testEmail,
handler: testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@ -27,85 +27,71 @@
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import * as os from '@/os';
import { unique } from '@/scripts/array';
import { i18n } from '@/i18n';
import { emojiCategories } from '@/instance';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
const props = defineProps<{
emoji: any,
}>();
props: {
emoji: {
required: true,
let dialog = $ref(null);
let name: string = $ref(props.emoji.name);
let category: string = $ref(props.emoji.category);
let aliases: string = $ref(props.emoji.aliases.join(' '));
let categories: string[] = $ref(emojiCategories);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
(ev: 'closed'): void
}>();
function ok() {
update();
}
async function update() {
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
});
emit('done', {
updated: {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
}
},
});
emits: ['done', 'closed'],
dialog.close();
}
data() {
return {
name: this.emoji.name,
category: this.emoji.category,
aliases: this.emoji.aliases?.join(' '),
categories: [],
}
},
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: name }),
});
if (canceled) return;
created() {
os.api('meta', { detail: false }).then(({ emojis }) => {
this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
os.api('admin/emoji/delete', {
id: props.emoji.id
}).then(() => {
emit('done', {
deleted: true
});
},
methods: {
ok() {
this.update();
},
async update() {
await os.apiWithDialog('admin/emoji/update', {
id: this.emoji.id,
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
});
this.$emit('done', {
updated: {
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
}
});
this.$refs.dialog.close();
},
async del() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.emoji.name }),
});
if (canceled) return;
os.api('admin/emoji/delete', {
id: this.emoji.id
}).then(() => {
this.$emit('done', {
deleted: true
});
this.$refs.dialog.close();
});
},
}
});
dialog.close();
});
}
</script>
<style lang="scss" scoped>

View File

@ -63,7 +63,7 @@
</template>
<script lang="ts" setup>
import { computed, defineComponent, ref, toRef } from 'vue';
import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
@ -130,17 +130,17 @@ const add = async (ev: MouseEvent) => {
};
const edit = (emoji) => {
os.popup(import('./emoji-edit-dialog.vue'), {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
emoji: emoji
}, {
done: result => {
if (result.updated) {
emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
...emoji,
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
...oldEmoji,
...result.updated
});
}));
} else if (result.deleted) {
emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
}
},
}, 'closed');
@ -159,7 +159,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
}, {
text: i18n.ts.import,
icon: 'fas fa-plus',
action: () => { im(emoji) }
action: () => { im(emoji); }
}], ev.currentTarget ?? ev.target);
};
@ -175,10 +175,10 @@ const menu = (ev: MouseEvent) => {
type: 'info',
text: i18n.ts.exportRequested,
});
}).catch((e) => {
}).catch((err) => {
os.alert({
type: 'error',
text: e.message,
text: err.message,
});
});
}
@ -195,10 +195,10 @@ const menu = (ev: MouseEvent) => {
type: 'info',
text: i18n.ts.importRequested,
});
}).catch((e) => {
}).catch((err) => {
os.alert({
type: 'error',
text: e.message,
text: err.message,
});
});
}

View File

@ -34,74 +34,52 @@
</XModalWindow>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkSwitch,
XModalWindow,
MkDriveFileThumbnail,
},
let file: any = $ref(null);
let info: any = $ref(null);
let isSensitive: boolean = $ref(false);
props: {
fileId: {
required: true,
}
},
const props = defineProps<{
fileId: string,
}>();
emits: ['closed'],
async function fetch() {
file = await os.api('drive/files/show', { fileId: props.fileId });
info = await os.api('admin/drive/show-file', { fileId: props.fileId });
isSensitive = file.isSensitive;
}
data() {
return {
file: null,
info: null,
isSensitive: false,
};
},
fetch();
created() {
this.fetch();
},
function showUser() {
os.pageWindow(`/user-info/${file.userId}`);
}
methods: {
async fetch() {
this.file = await os.api('drive/files/show', { fileId: this.fileId });
this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
this.isSensitive = this.file.isSensitive;
},
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: file.name }),
});
if (canceled) return;
showUser() {
os.pageWindow(`/user-info/${this.file.userId}`);
},
os.apiWithDialog('drive/files/delete', {
fileId: file.id
});
}
async del() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.file.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: this.file.id
});
},
async toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
this.isSensitive = v;
},
bytes
}
});
async function toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
isSensitive = v;
}
</script>
<style lang="scss" scoped>

View File

@ -55,7 +55,7 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, defineAsyncComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@ -93,7 +93,7 @@ function clear() {
}
function show(file) {
os.popup(import('./file-dialog.vue'), {
os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), {
fileId: file.id
}, {}, 'closed');
}

View File

@ -1,6 +1,6 @@
<template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
<div v-if="!narrow || page == null" class="nav">
<div v-if="!narrow || initialPage == null" class="nav">
<MkHeader :info="header"></MkHeader>
<MkSpacer :content-max="700" :margin-min="16">
@ -12,21 +12,21 @@
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
</div>
</MkSpacer>
</div>
<div class="main">
<div v-if="!(narrow && initialPage == null)" class="main">
<MkStickyContainer>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
</MkStickyContainer>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
import { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue';
import MkInfo from '@/components/ui/info.vue';
@ -35,298 +35,283 @@ import { instance } from '@/instance';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { lookupUser } from '@/scripts/lookup-user';
import { MisskeyNavigator } from '@/scripts/navigate';
export default defineComponent({
components: {
MkSuperMenu,
MkInfo,
},
const isEmpty = (x: string | null) => x == null || x === '';
provide: {
shouldOmitHeaderTitle: false,
},
const nav = new MisskeyNavigator();
props: {
initialPage: {
type: String,
required: false
const indexInfo = {
title: i18n.ts.controlPanel,
icon: 'fas fa-cog',
bg: 'var(--bg)',
hideHeader: true,
};
const props = defineProps<{
initialPage?: string,
}>();
provide('shouldOmitHeaderTitle', false);
let INFO = $ref(indexInfo);
let childInfo = $ref(null);
let page = $ref(props.initialPage);
let narrow = $ref(false);
let view = $ref(null);
let el = $ref(null);
let pageProps = $ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
const NARROW_THRESHOLD = 600;
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = $computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
icon: 'fas fa-search',
text: i18n.ts.lookup,
action: lookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'fas fa-user',
text: i18n.ts.invite,
action: invite,
}] : [])],
}, {
title: i18n.ts.administration,
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.ts.dashboard,
to: '/admin/overview',
active: props.initialPage === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.ts.users,
to: '/admin/users',
active: props.initialPage === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: props.initialPage === 'emojis',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.emojiGen,
to: '/admin/emojigen',
active: props.initialPage === 'emojigen',
}, {
icon: 'fas fa-globe',
text: i18n.ts.federation,
to: '/admin/federation',
active: props.initialPage === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.ts.jobQueue,
to: '/admin/queue',
active: props.initialPage === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.files,
to: '/admin/files',
active: props.initialPage === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.ts.announcements,
to: '/admin/announcements',
active: props.initialPage === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: props.initialPage === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: props.initialPage === 'abuses',
}],
}, {
title: i18n.ts.settings,
items: [{
icon: 'fas fa-cog',
text: i18n.ts.general,
to: '/admin/settings',
active: props.initialPage === 'settings',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
active: props.initialPage === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
active: props.initialPage === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/admin/security',
active: props.initialPage === 'security',
}, {
icon: 'fas fa-globe',
text: i18n.ts.relays,
to: '/admin/relays',
active: props.initialPage === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/admin/integrations',
active: props.initialPage === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: props.initialPage === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: props.initialPage === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.ts.other,
to: '/admin/other-settings',
active: props.initialPage === 'other-settings',
}],
}, {
title: i18n.ts.info,
items: [{
icon: 'fas fa-database',
text: i18n.ts.database,
to: '/admin/database',
active: props.initialPage === 'database',
}],
}]);
const component = $computed(() => {
if (props.initialPage == null) return null;
switch (props.initialPage) {
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
case 'users': return defineAsyncComponent(() => import('./users.vue'));
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
case 'emojigen': return defineAsyncComponent(() => import('./emojigen.vue'));
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
}
});
watch(component, () => {
pageProps = {};
nextTick(() => {
scroll(el, { top: 0 });
});
}, { immediate: true });
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
} else {
if (props.initialPage == null) {
INFO = indexInfo;
}
},
}
});
setup(props, context) {
const indexInfo = {
title: i18n.ts.controlPanel,
icon: 'fas fa-cog',
bg: 'var(--bg)',
hideHeader: true,
};
const INFO = ref(indexInfo);
const childInfo = ref(null);
const page = ref(props.initialPage);
const narrow = ref(false);
const view = ref(null);
const el = ref(null);
const pageChanged = (page) => {
if (page == null) return;
const viewInfo = page[symbols.PAGE_INFO];
if (isRef(viewInfo)) {
watch(viewInfo, () => {
childInfo.value = viewInfo.value;
}, { immediate: true });
} else {
childInfo.value = viewInfo;
}
};
const pageProps = ref({});
watch(narrow, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
}
});
const isEmpty = (x: any) => x == null || x == '';
onMounted(() => {
ro.observe(el);
const noMaintainerInformation = ref(false);
const noBotProtection = ref(false);
narrow = el.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
}
});
os.api('meta', { detail: true }).then(meta => {
// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
onUnmounted(() => {
ro.disconnect();
});
const pageChanged = (page) => {
if (page == null) {
childInfo = null;
} else {
childInfo = page[symbols.PAGE_INFO];
}
};
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
});
const menuDef = computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
icon: 'fas fa-search',
text: i18n.ts.lookup,
action: lookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'fas fa-user',
text: i18n.ts.invite,
action: invite,
}] : [])],
}, {
title: i18n.ts.administration,
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.ts.dashboard,
to: '/admin/overview',
active: page.value === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.ts.users,
to: '/admin/users',
active: page.value === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: page.value === 'emojis',
},{
icon: 'fas fa-laugh',
text: i18n.ts.emojiGen,
to: '/admin/emojigen',
active: page.value === 'emojigen',
},{
icon: 'fas fa-globe',
text: i18n.ts.federation,
to: '/admin/federation',
active: page.value === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.ts.jobQueue,
to: '/admin/queue',
active: page.value === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.files,
to: '/admin/files',
active: page.value === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.ts.announcements,
to: '/admin/announcements',
active: page.value === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: page.value === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: page.value === 'abuses',
}],
}, {
title: i18n.ts.settings,
items: [{
icon: 'fas fa-cog',
text: i18n.ts.general,
to: '/admin/settings',
active: page.value === 'settings',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
active: page.value === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
active: page.value === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/admin/security',
active: page.value === 'security',
}, {
icon: 'fas fa-globe',
text: i18n.ts.relays,
to: '/admin/relays',
active: page.value === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/admin/integrations',
active: page.value === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: page.value === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: page.value === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.ts.other,
to: '/admin/other-settings',
active: page.value === 'other-settings',
}],
}, {
title: i18n.ts.info,
items: [{
icon: 'fas fa-database',
text: i18n.ts.database,
to: '/admin/database',
active: page.value === 'database',
}],
}]);
const component = computed(() => {
if (page.value == null) return null;
switch (page.value) {
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
case 'users': return defineAsyncComponent(() => import('./users.vue'));
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
case 'emojigen': return defineAsyncComponent(() => import('./emojigen.vue'));
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
}
}).catch(err => {
os.alert({
type: 'error',
text: err,
});
});
};
watch(component, () => {
pageProps.value = {};
const lookup = (ev) => {
os.popupMenu([{
text: i18n.ts.user,
icon: 'fas fa-user',
action: () => {
lookupUser();
}
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
}], ev.currentTarget ?? ev.target);
};
nextTick(() => {
scroll(el.value, { top: 0 });
});
}, { immediate: true });
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow.value) {
page.value = 'overview';
} else {
page.value = props.initialPage;
if (props.initialPage == null) {
INFO.value = indexInfo;
}
}
});
onMounted(() => {
narrow.value = el.value.offsetWidth < 800;
if (!narrow.value) {
page.value = 'overview';
}
});
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
};
const lookup = (ev) => {
os.popupMenu([{
text: i18n.ts.user,
icon: 'fas fa-user',
action: () => {
lookupUser();
}
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
}], ev.currentTarget ?? ev.target);
};
return {
[symbols.PAGE_INFO]: INFO,
menuDef,
header: {
title: i18n.ts.controlPanel,
},
noMaintainerInformation,
noBotProtection,
page,
narrow,
view,
el,
pageChanged,
childInfo,
pageProps,
component,
invite,
lookup,
};
},
defineExpose({
[symbols.PAGE_INFO]: INFO,
header: {
title: i18n.ts.controlPanel,
}
});
</script>

View File

@ -2,57 +2,45 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ $ts.blockedInstances }}</span>
<template #caption>{{ $ts.blockedInstancesDescription }}</template>
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormButton,
FormTextarea,
FormSuspense,
},
let blockedHosts: string = $ref('');
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
blockedHosts = meta.blockedHosts.join('\n');
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
},
blockedHosts: '',
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.blockedHosts = meta.blockedHosts.join('\n');
},
save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: this.blockedHosts.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
}
});
</script>

View File

@ -24,57 +24,36 @@
</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableDiscordIntegration: boolean = $ref(false);
let discordClientId: string | null = $ref(null);
let discordClientSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableDiscordIntegration = meta.enableDiscordIntegration;
discordClientId = meta.discordClientId;
discordClientSecret = meta.discordClientSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'Discord',
icon: 'fab fa-discord'
},
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId;
this.discordClientSecret = meta.discordClientSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration,
discordClientId,
discordClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@ -24,57 +24,36 @@
</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableGithubIntegration: boolean = $ref(false);
let githubClientId: string | null = $ref(null);
let githubClientSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableGithubIntegration = meta.enableGithubIntegration;
githubClientId = meta.githubClientId;
githubClientSecret = meta.githubClientSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'GitHub',
icon: 'fab fa-github'
},
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.githubClientId = meta.githubClientId;
this.githubClientSecret = meta.githubClientSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration,
githubClientId,
githubClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@ -24,7 +24,7 @@
</FormSuspense>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
@ -32,49 +32,28 @@ import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableTwitterIntegration: boolean = $ref(false);
let twitterConsumerKey: string | null = $ref(null);
let twitterConsumerSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableTwitterIntegration = meta.enableTwitterIntegration;
twitterConsumerKey = meta.twitterConsumerKey;
twitterConsumerSecret = meta.twitterConsumerSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'Twitter',
icon: 'fab fa-twitter'
},
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.twitterConsumerKey = meta.twitterConsumerKey;
this.twitterConsumerSecret = meta.twitterConsumerSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration,
twitterConsumerKey,
twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@ -4,69 +4,52 @@
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-twitter"></i></template>
<template #label>Twitter</template>
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XTwitter/>
</FormFolder>
<FormFolder to="/admin/integrations/github" class="_formBlock">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-github"></i></template>
<template #label>GitHub</template>
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XGithub/>
</FormFolder>
<FormFolder to="/admin/integrations/discord" class="_formBlock">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-discord"></i></template>
<template #label>Discord</template>
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XDiscord/>
</FormFolder>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSecion from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormFolder,
FormSecion,
FormSuspense,
XTwitter,
XGithub,
XDiscord,
},
let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false);
let enableDiscordIntegration: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
enableTwitterIntegration = meta.enableTwitterIntegration;
enableGithubIntegration = meta.enableGithubIntegration;
enableDiscordIntegration = meta.enableDiscordIntegration;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
},
enableTwitterIntegration: false,
enableGithubIntegration: false,
enableDiscordIntegration: false,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
}
});
</script>

View File

@ -132,7 +132,7 @@ export default defineComponent({
overviewHeight: '1fr',
queueHeight: '1fr',
paused: false,
}
};
},
computed: {

View File

@ -2,32 +2,32 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch>
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<template #label>{{ $ts.objectStorageBaseUrl }}</template>
<template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template>
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageBucket" class="_formBlock">
<template #label>{{ $ts.objectStorageBucket }}</template>
<template #caption>{{ $ts.objectStorageBucketDesc }}</template>
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput>
<FormInput v-model="objectStoragePrefix" class="_formBlock">
<template #label>{{ $ts.objectStoragePrefix }}</template>
<template #caption>{{ $ts.objectStoragePrefixDesc }}</template>
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
<template #label>{{ $ts.objectStorageEndpoint }}</template>
<template #caption>{{ $ts.objectStorageEndpointDesc }}</template>
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageRegion" class="_formBlock">
<template #label>{{ $ts.objectStorageRegion }}</template>
<template #caption>{{ $ts.objectStorageRegionDesc }}</template>
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
<FormSplit :min-width="280">
@ -43,17 +43,17 @@
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
<template #label>{{ $ts.objectStorageUseSSL }}</template>
<template #caption>{{ $ts.objectStorageUseSSLDesc }}</template>
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
<template #label>{{ $ts.objectStorageUseProxy }}</template>
<template #caption>{{ $ts.objectStorageUseProxyDesc }}</template>
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
<template #label>{{ $ts.objectStorageSetPublicRead }}</template>
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
@ -65,8 +65,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormGroup from '@/components/form/group.vue';
@ -76,84 +76,70 @@ import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormGroup,
FormSuspense,
FormSplit,
FormSection,
},
let useObjectStorage: boolean = $ref(false);
let objectStorageBaseUrl: string | null = $ref(null);
let objectStorageBucket: string | null = $ref(null);
let objectStoragePrefix: string | null = $ref(null);
let objectStorageEndpoint: string | null = $ref(null);
let objectStorageRegion: string | null = $ref(null);
let objectStoragePort: number | null = $ref(null);
let objectStorageAccessKey: string | null = $ref(null);
let objectStorageSecretKey: string | null = $ref(null);
let objectStorageUseSSL: boolean = $ref(false);
let objectStorageUseProxy: boolean = $ref(false);
let objectStorageSetPublicRead: boolean = $ref(false);
let objectStorageS3ForcePathStyle: boolean = $ref(true);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
useObjectStorage = meta.useObjectStorage;
objectStorageBaseUrl = meta.objectStorageBaseUrl;
objectStorageBucket = meta.objectStorageBucket;
objectStoragePrefix = meta.objectStoragePrefix;
objectStorageEndpoint = meta.objectStorageEndpoint;
objectStorageRegion = meta.objectStorageRegion;
objectStoragePort = meta.objectStoragePort;
objectStorageAccessKey = meta.objectStorageAccessKey;
objectStorageSecretKey = meta.objectStorageSecretKey;
objectStorageUseSSL = meta.objectStorageUseSSL;
objectStorageUseProxy = meta.objectStorageUseProxy;
objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
useObjectStorage: false,
objectStorageBaseUrl: null,
objectStorageBucket: null,
objectStoragePrefix: null,
objectStorageEndpoint: null,
objectStorageRegion: null,
objectStoragePort: null,
objectStorageAccessKey: null,
objectStorageSecretKey: null,
objectStorageUseSSL: false,
objectStorageUseProxy: false,
objectStorageSetPublicRead: false,
objectStorageS3ForcePathStyle: true,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
useObjectStorage,
objectStorageBaseUrl,
objectStorageBucket,
objectStoragePrefix,
objectStorageEndpoint,
objectStorageRegion,
objectStoragePort,
objectStorageAccessKey,
objectStorageSecretKey,
objectStorageUseSSL,
objectStorageUseProxy,
objectStorageSetPublicRead,
objectStorageS3ForcePathStyle,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.useObjectStorage = meta.useObjectStorage;
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
this.objectStorageBucket = meta.objectStorageBucket;
this.objectStoragePrefix = meta.objectStoragePrefix;
this.objectStorageEndpoint = meta.objectStorageEndpoint;
this.objectStorageRegion = meta.objectStorageRegion;
this.objectStoragePort = meta.objectStoragePort;
this.objectStorageAccessKey = meta.objectStorageAccessKey;
this.objectStorageSecretKey = meta.objectStorageSecretKey;
this.objectStorageUseSSL = meta.objectStorageUseSSL;
this.objectStorageUseProxy = meta.objectStorageUseProxy;
this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
},
save() {
os.apiWithDialog('admin/update-meta', {
useObjectStorage: this.useObjectStorage,
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
objectStorageUseSSL: this.objectStorageUseSSL,
objectStorageUseProxy: this.objectStorageUseProxy,
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@ -6,52 +6,35 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormSection from '@/components/form/section.vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSection,
FormSuspense,
},
async function init() {
await os.api('admin/meta');
}
emits: ['info'],
function save() {
os.apiWithDialog('admin/update-meta').then(() => {
fetchInstance();
});
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
},
save() {
os.apiWithDialog('admin/update-meta', {
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@ -5,20 +5,20 @@
<div class="label">Users</div>
<div class="value _monospace">
{{ number(stats.originalUsersCount) }}
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="$ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
</div>
</div>
<div class="number _panel">
<div class="label">Notes</div>
<div class="value _monospace">
{{ number(stats.originalNotesCount) }}
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="$ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
</div>
</div>
</div>
<MkContainer :foldable="true" class="charts">
<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
<template #header><i class="fas fa-chart-bar"></i>{{ i18n.ts.charts }}</template>
<div style="padding: 12px;">
<MkInstanceStats :chart-limit="500" :detailed="true"/>
</div>
@ -38,7 +38,7 @@
<!--<XMetrics/>-->
<MkFolder style="margin: var(--margin)">
<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
<div class="cfcdecdf">
<div class="number _panel">
<div class="label">Misskey</div>
@ -65,103 +65,61 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/form/select.vue';
import MkNumberDiff from '@/components/number-diff.vue';
import MkContainer from '@/components/ui/container.vue';
import MkFolder from '@/components/ui/folder.vue';
import MkQueueChart from '@/components/queue-chart.vue';
import { version, url } from '@/config';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import XMetrics from './metrics.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkNumberDiff,
MkInstanceStats,
MkContainer,
MkFolder,
MkQueueChart,
XMetrics,
},
let stats: any = $ref(null);
let serverInfo: any = $ref(null);
let usersComparedToThePrevDay: any = $ref(null);
let notesComparedToThePrevDay: any = $ref(null);
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
emits: ['info'],
onMounted(async () => {
os.api('stats', {}).then(statsResponse => {
stats = statsResponse;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
},
version,
vueVersion,
url,
stats: null,
meta: null,
serverInfo: null,
usersComparedToThePrevDay: null,
notesComparedToThePrevDay: null,
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
queueStatsConnection: markRaw(stream.useChannel('queueStats')),
}
},
async mounted() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
os.api('stats', {}).then(stats => {
this.stats = stats;
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
});
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
});
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
});
os.api('admin/server-info', {}).then(serverInfo => {
this.serverInfo = serverInfo;
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
});
});
this.$nextTick(() => {
this.queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
os.api('admin/server-info').then(serverInfoResponse => {
serverInfo = serverInfoResponse;
});
nextTick(() => {
queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
},
});
});
beforeUnmount() {
this.queueStatsConnection.dispose();
},
onBeforeUnmount(() => {
queueStatsConnection.dispose();
});
methods: {
async showInstanceInfo(q) {
let instance = q;
if (typeof q === 'string') {
instance = await os.api('federation/show-instance', {
host: q
});
}
// TODO
},
bytes,
number,
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,19 +1,19 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo>
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
<template #key>{{ i18n.ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
</MkKeyValue>
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton>
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkKeyValue from '@/components/key-value.vue';
import FormButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
@ -21,53 +21,40 @@ import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkKeyValue,
FormButton,
MkInfo,
FormSuspense,
},
let proxyAccount: any = $ref(null);
let proxyAccountId: any = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
proxyAccountId = meta.proxyAccountId;
if (proxyAccountId) {
proxyAccount = await os.api('users/show', { userId: proxyAccountId });
}
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
},
proxyAccount: null,
proxyAccountId: null,
}
},
function chooseProxyAccount() {
os.selectUser().then(user => {
proxyAccount = user;
proxyAccountId = user.id;
save();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.proxyAccountId = meta.proxyAccountId;
if (this.proxyAccountId) {
this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: proxyAccountId,
}).then(() => {
fetchInstance();
});
}
chooseProxyAccount() {
os.selectUser().then(user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save();
});
},
save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: this.proxyAccountId,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
}
});
</script>

View File

@ -26,62 +26,40 @@
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import number from '@/filters/number';
import MkQueueChart from '@/components/queue-chart.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkQueueChart
},
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
props: {
domain: {
type: String,
required: true,
},
connection: {
required: true,
},
},
const props = defineProps<{
domain: string,
connection: any,
}>();
setup(props) {
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
jobs.value = result;
});
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
jobs.value = result;
});
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
props.connection.on('stats', onStats);
props.connection.on('stats', onStats);
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
return {
jobs,
activeSincePrevTick,
active,
waiting,
delayed,
number,
};
},
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
</script>

View File

@ -6,71 +6,60 @@
<XQueue :connection="connection" domain="deliver">
<template #title>Out</template>
</XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
<script lang="ts" setup>
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import * as config from '@/config';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
XQueue,
},
const connection = markRaw(stream.useChannel('queueStats'));
emits: ['info'],
function clear() {
os.confirm({
type: 'warning',
title: i18n.ts.clearQueueConfirmTitle,
text: i18n.ts.clearQueueConfirmText,
}).then(({ canceled }) => {
if (canceled) return;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: this.$ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
},
connection: markRaw(stream.useChannel('queueStats')),
}
},
os.apiWithDialog('admin/queue/clear');
});
}
mounted() {
this.$nextTick(() => {
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
onMounted(() => {
nextTick(() => {
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
},
});
});
beforeUnmount() {
this.connection.dispose();
},
onBeforeUnmount(() => {
connection.dispose();
});
methods: {
clear() {
os.confirm({
type: 'warning',
title: this.$ts.clearQueueConfirmTitle,
text: this.$ts.clearQueueConfirmText,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('admin/queue/clear', {});
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: i18n.ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
}
});
</script>

View File

@ -8,84 +8,71 @@
<i v-else class="fas fa-clock icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
},
let relays: any[] = $ref([]);
emits: ['info'],
async function addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: i18n.ts.addRelay,
type: 'url',
placeholder: i18n.ts.inboxUrl
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
});
});
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.addRelay,
handler: this.addRelay,
}],
},
relays: [],
inbox: '',
}
},
function remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
}).then(() => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
});
});
}
created() {
this.refresh();
},
function refresh() {
os.api('admin/relays/list').then((relayList: any) => {
relays = relayList;
});
}
methods: {
async addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: this.$ts.addRelay,
type: 'url',
placeholder: this.$ts.inboxUrl
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
this.refresh();
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message || e
});
});
},
refresh();
remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
}).then(() => {
this.refresh();
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message || e
});
});
},
refresh() {
os.api('admin/relays/list').then((relays: any) => {
this.relays = relays;
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addRelay,
handler: addRelay,
}],
}
});
</script>

View File

@ -4,10 +4,10 @@
<div class="_formRoot">
<FormFolder class="_formBlock">
<template #icon><i class="fas fa-shield-alt"></i></template>
<template #label>{{ $ts.botProtection }}</template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<XBotProtection/>
</FormFolder>
@ -21,7 +21,7 @@
<template #label>Summaly Proxy URL</template>
</FormInput>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</div>
</FormFolder>
</div>
@ -29,8 +29,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInfo from '@/components/ui/info.vue';
@ -42,49 +42,32 @@ import XBotProtection from './bot-protection.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormFolder,
FormSwitch,
FormInfo,
FormSection,
FormSuspense,
FormButton,
FormInput,
XBotProtection,
},
let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false);
let enableRecaptcha: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
summalyProxy = meta.summalyProxy;
enableHcaptcha = meta.enableHcaptcha;
enableRecaptcha = meta.enableRecaptcha;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
},
summalyProxy: '',
enableHcaptcha: false,
enableRecaptcha: false,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
summalyProxy,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.summalyProxy = meta.summalyProxy;
this.enableHcaptcha = meta.enableHcaptcha;
this.enableRecaptcha = meta.enableRecaptcha;
},
save() {
os.apiWithDialog('admin/update-meta', {
summalyProxy: this.summalyProxy,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
}
});
</script>

View File

@ -3,104 +3,104 @@
<FormSuspense :p="init">
<div class="_formRoot">
<FormInput v-model="name" class="_formBlock">
<template #label>{{ $ts.instanceName }}</template>
<template #label>{{ i18n.ts.instanceName }}</template>
</FormInput>
<FormTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.instanceDescription }}</template>
<template #label>{{ i18n.ts.instanceDescription }}</template>
</FormTextarea>
<FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.tosUrl }}</template>
<template #label>{{ i18n.ts.tosUrl }}</template>
</FormInput>
<FormSplit :min-width="300">
<FormInput v-model="maintainerName" class="_formBlock">
<template #label>{{ $ts.maintainerName }}</template>
<template #label>{{ i18n.ts.maintainerName }}</template>
</FormInput>
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #prefix><i class="fas fa-envelope"></i></template>
<template #label>{{ $ts.maintainerEmail }}</template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
</FormInput>
</FormSplit>
<FormTextarea v-model="pinnedUsers" class="_formBlock">
<template #label>{{ $ts.pinnedUsers }}</template>
<template #caption>{{ $ts.pinnedUsersDescription }}</template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</FormTextarea>
<FormSection>
<FormSwitch v-model="enableRegistration" class="_formBlock">
<template #label>{{ $ts.enableRegistration }}</template>
<template #label>{{ i18n.ts.enableRegistration }}</template>
</FormSwitch>
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
<template #label>{{ $ts.emailRequiredForSignup }}</template>
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</FormSwitch>
</FormSection>
<FormSection>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ $ts.theme }}</template>
<template #label>{{ i18n.ts.theme }}</template>
<FormInput v-model="iconUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.iconUrl }}</template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</FormInput>
<FormInput v-model="bannerUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.bannerUrl }}</template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</FormInput>
<FormInput v-model="backgroundImageUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.backgroundImageUrl }}</template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</FormInput>
<FormInput v-model="themeColor" class="_formBlock">
<template #prefix><i class="fas fa-palette"></i></template>
<template #label>{{ $ts.themeColor }}</template>
<template #label>{{ i18n.ts.themeColor }}</template>
<template #caption>#RRGGBB</template>
</FormInput>
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
<template #label>{{ $ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
<template #label>{{ $ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
</FormSection>
<FormSection>
<template #label>{{ $ts.files }}</template>
<template #label>{{ i18n.ts.files }}</template>
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.cacheRemoteFiles }}</template>
<template #caption>{{ $ts.cacheRemoteFilesDescription }}</template>
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSplit :min-width="280">
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerLocalAccount }}</template>
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template>
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
@ -109,8 +109,8 @@
<template #label>ServiceWorker</template>
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
<template #label>{{ $ts.enableServiceworker }}</template>
<template #caption>{{ $ts.serviceworkerInfo }}</template>
<template #label>{{ i18n.ts.enableServiceworker }}</template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</FormSwitch>
<template v-if="enableServiceWorker">
@ -142,8 +142,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@ -154,119 +154,103 @@ import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSuspense,
FormTextarea,
FormInfo,
FormSection,
FormSplit,
},
let name: string | null = $ref(null);
let description: string | null = $ref(null);
let tosUrl: string | null = $ref(null);
let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null);
let iconUrl: string | null = $ref(null);
let bannerUrl: string | null = $ref(null);
let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
let defaultLightTheme: any = $ref(null);
let defaultDarkTheme: any = $ref(null);
let enableLocalTimeline: boolean = $ref(false);
let enableGlobalTimeline: boolean = $ref(false);
let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false);
let localDriveCapacityMb: any = $ref(0);
let remoteDriveCapacityMb: any = $ref(0);
let enableRegistration: boolean = $ref(false);
let emailRequiredForSignup: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref('');
let deeplIsPro: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
name = meta.name;
description = meta.description;
tosUrl = meta.tosUrl;
iconUrl = meta.iconUrl;
bannerUrl = meta.bannerUrl;
backgroundImageUrl = meta.backgroundImageUrl;
themeColor = meta.themeColor;
defaultLightTheme = meta.defaultLightTheme;
defaultDarkTheme = meta.defaultDarkTheme;
maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail;
enableLocalTimeline = !meta.disableLocalTimeline;
enableGlobalTimeline = !meta.disableGlobalTimeline;
pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles;
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
enableRegistration = !meta.disableRegistration;
emailRequiredForSignup = meta.emailRequiredForSignup;
enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
name: null,
description: null,
tosUrl: null as string | null,
maintainerName: null,
maintainerEmail: null,
iconUrl: null,
bannerUrl: null,
backgroundImageUrl: null,
themeColor: null,
defaultLightTheme: null,
defaultDarkTheme: null,
enableLocalTimeline: false,
enableGlobalTimeline: false,
pinnedUsers: '',
cacheRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
enableRegistration: false,
emailRequiredForSignup: false,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
deeplAuthKey: '',
deeplIsPro: false,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
name,
description,
tosUrl,
iconUrl,
bannerUrl,
backgroundImageUrl,
themeColor: themeColor === '' ? null : themeColor,
defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
maintainerName,
maintainerEmail,
disableLocalTimeline: !enableLocalTimeline,
disableGlobalTimeline: !enableGlobalTimeline,
pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles,
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
disableRegistration: !enableRegistration,
emailRequiredForSignup,
enableServiceWorker,
swPublicKey,
swPrivateKey,
deeplAuthKey,
deeplIsPro,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.name = meta.name;
this.description = meta.description;
this.tosUrl = meta.tosUrl;
this.iconUrl = meta.iconUrl;
this.bannerUrl = meta.bannerUrl;
this.backgroundImageUrl = meta.backgroundImageUrl;
this.themeColor = meta.themeColor;
this.defaultLightTheme = meta.defaultLightTheme;
this.defaultDarkTheme = meta.defaultDarkTheme;
this.maintainerName = meta.maintainerName;
this.maintainerEmail = meta.maintainerEmail;
this.enableLocalTimeline = !meta.disableLocalTimeline;
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
this.pinnedUsers = meta.pinnedUsers.join('\n');
this.cacheRemoteFiles = meta.cacheRemoteFiles;
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
this.enableRegistration = !meta.disableRegistration;
this.emailRequiredForSignup = meta.emailRequiredForSignup;
this.enableServiceWorker = meta.enableServiceWorker;
this.swPublicKey = meta.swPublickey;
this.swPrivateKey = meta.swPrivateKey;
this.deeplAuthKey = meta.deeplAuthKey;
this.deeplIsPro = meta.deeplIsPro;
},
save() {
os.apiWithDialog('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
iconUrl: this.iconUrl,
bannerUrl: this.bannerUrl,
backgroundImageUrl: this.backgroundImageUrl,
themeColor: this.themeColor === '' ? null : this.themeColor,
defaultLightTheme: this.defaultLightTheme === '' ? null : this.defaultLightTheme,
defaultDarkTheme: this.defaultDarkTheme === '' ? null : this.defaultDarkTheme,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
pinnedUsers: this.pinnedUsers.split('\n'),
cacheRemoteFiles: this.cacheRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
disableRegistration: !this.enableRegistration,
emailRequiredForSignup: this.emailRequiredForSignup,
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
deeplAuthKey: this.deeplAuthKey,
deeplIsPro: this.deeplIsPro,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@ -25,72 +25,60 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
<script lang="ts" setup>
import { ref } from 'vue';
import JSON5 from 'json5';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { Endpoints } from 'misskey-js';
export default defineComponent({
components: {
MkButton, MkInput, MkTextarea, MkSwitch,
},
const body = ref('{}');
const endpoint = ref('');
const endpoints = ref<any[]>([]);
const sending = ref(false);
const res = ref('');
const withCredential = ref(true);
data() {
return {
[symbols.PAGE_INFO]: {
title: 'API console',
icon: 'fas fa-terminal'
},
os.api('endpoints').then(endpointResponse => {
endpoints.value = endpointResponse;
});
endpoint: '',
body: '{}',
res: null,
sending: false,
endpoints: [],
withCredential: true,
function send() {
sending.value = true;
const requestBody = JSON5.parse(body.value);
os.api(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => {
sending.value = false;
res.value = JSON5.stringify(resp, null, 2);
}, err => {
sending.value = false;
res.value = JSON5.stringify(err, null, 2);
});
}
};
},
created() {
os.api('endpoints').then(endpoints => {
this.endpoints = endpoints;
});
},
methods: {
send() {
this.sending = true;
const body = JSON5.parse(this.body);
os.api(this.endpoint, body, body.i || (this.withCredential ? undefined : null)).then(res => {
this.sending = false;
this.res = JSON5.stringify(res, null, 2);
}, err => {
this.sending = false;
this.res = JSON5.stringify(err, null, 2);
});
},
onEndpointChange() {
os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => {
const body = {};
for (const p of endpoint.params) {
body[p.name] =
p.type === 'String' ? '' :
p.type === 'Number' ? 0 :
p.type === 'Boolean' ? false :
p.type === 'Array' ? [] :
p.type === 'Object' ? {} :
null;
}
this.body = JSON5.stringify(body, null, 2);
});
function onEndpointChange() {
os.api('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => {
const endpointBody = {};
for (const p of resp.params) {
endpointBody[p.name] =
p.type === 'String' ? '' :
p.type === 'Number' ? 0 :
p.type === 'Boolean' ? false :
p.type === 'Array' ? [] :
p.type === 'Object' ? {} :
null;
}
}
body.value = JSON5.stringify(endpointBody, null, 2);
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: 'API console',
icon: 'fas fa-terminal'
},
});
</script>

View File

@ -32,7 +32,7 @@ export default defineComponent({
computed: {
name(): string {
const el = document.createElement('div');
el.textContent = this.app.name
el.textContent = this.app.name;
return el.innerHTML;
},
app(): any {

View File

@ -111,8 +111,8 @@ export default defineComponent({
}
},
setBannerImage(e) {
selectFile(e.currentTarget ?? e.target, null).then(file => {
setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
this.bannerId = file.id;
});
},

View File

@ -58,7 +58,7 @@ export default defineComponent({
tags: emojiTags,
selectedTags: new Set(),
searchEmojis: null,
}
};
},
watch: {
@ -79,9 +79,9 @@ export default defineComponent({
}
if (this.selectedTags.size === 0) {
this.searchEmojis = this.customEmojis.filter(e => e.name.includes(this.q) || e.aliases.includes(this.q));
this.searchEmojis = this.customEmojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q));
} else {
this.searchEmojis = this.customEmojis.filter(e => (e.name.includes(this.q) || e.aliases.includes(this.q)) && [...this.selectedTags].every(t => e.aliases.includes(t)));
this.searchEmojis = this.customEmojis.filter(emoji => (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) && [...this.selectedTags].every(t => emoji.aliases.includes(t)));
}
},

View File

@ -25,10 +25,10 @@ function menu(ev) {
type: 'info',
text: i18n.ts.exportRequested,
});
}).catch((e) => {
}).catch((err) => {
os.alert({
type: 'error',
text: e.message,
text: err.message,
});
});
}

View File

@ -127,7 +127,7 @@ function getStatus(instance) {
if (instance.isSuspended) return 'suspended';
if (instance.isNotResponding) return 'error';
return 'alive';
};
}
defineExpose({
[symbols.PAGE_INFO]: {

View File

@ -20,7 +20,7 @@ export default defineComponent({
uri: acct
});
promise.then(res => {
if (res.type == 'User') {
if (res.type === 'User') {
this.follow(res.object);
} else if (res.type === 'Note') {
this.$router.push(`/notes/${res.object.id}`);

View File

@ -71,7 +71,7 @@ export default defineComponent({
description: null,
title: null,
isSensitive: false,
}
};
},
watch: {
@ -91,8 +91,8 @@ export default defineComponent({
},
methods: {
selectFile(e) {
selectFiles(e.currentTarget ?? e.target, null).then(files => {
selectFile(evt) {
selectFiles(evt.currentTarget ?? evt.target, null).then(files => {
this.files = this.files.concat(files);
});
},

View File

@ -119,8 +119,8 @@ export default defineComponent({
postId: this.postId
}).then(post => {
this.post = post;
}).catch(e => {
this.error = e;
}).catch(err => {
this.error = err;
});
},

View File

@ -90,14 +90,14 @@ export default defineComponent({
getAcct: Acct.toString,
isMe(message) {
return message.userId == this.$i.id;
return message.userId === this.$i.id;
},
onMessage(message) {
if (message.recipientId) {
this.messages = this.messages.filter(m => !(
(m.recipientId == message.recipientId && m.userId == message.userId) ||
(m.recipientId == message.userId && m.userId == message.recipientId)));
(m.recipientId === message.recipientId && m.userId === message.userId) ||
(m.recipientId === message.userId && m.userId === message.recipientId)));
this.messages.unshift(message);
} else if (message.groupId) {
@ -108,7 +108,7 @@ export default defineComponent({
onRead(ids) {
for (const id of ids) {
const found = this.messages.find(m => m.id == id);
const found = this.messages.find(m => m.id === id);
if (found) {
if (found.recipientId) {
found.isRead = true;
@ -123,11 +123,11 @@ export default defineComponent({
os.popupMenu([{
text: this.$ts.messagingWithUser,
icon: 'fas fa-user',
action: () => { this.startUser() }
action: () => { this.startUser(); }
}, {
text: this.$ts.messagingWithGroup,
icon: 'fas fa-users',
action: () => { this.startGroup() }
action: () => { this.startGroup(); }
}], ev.currentTarget ?? ev.target);
},

View File

@ -31,6 +31,7 @@ import * as os from '@/os';
import { stream } from '@/stream';
import { Autocomplete } from '@/scripts/autocomplete';
import { throttle } from 'throttle-debounce';
import { uploadFile } from '@/scripts/upload';
export default defineComponent({
props: {
@ -58,7 +59,7 @@ export default defineComponent({
return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
},
canSend(): boolean {
return (this.text != null && this.text != '') || this.file != null;
return (this.text != null && this.text !== '') || this.file != null;
},
room(): any {
return this.$parent;
@ -87,12 +88,11 @@ export default defineComponent({
}
},
methods: {
async onPaste(e: ClipboardEvent) {
const data = e.clipboardData;
const items = data.items;
async onPaste(evt: ClipboardEvent) {
const items = evt.clipboardData.items;
if (items.length == 1) {
if (items[0].kind == 'file') {
if (items.length === 1) {
if (items[0].kind === 'file') {
const file = items[0].getAsFile();
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
@ -100,7 +100,7 @@ export default defineComponent({
if (formatted) this.upload(file, formatted);
}
} else {
if (items[0].kind == 'file') {
if (items[0].kind === 'file') {
os.alert({
type: 'error',
text: this.$ts.onlyOneFileCanBeAttached
@ -109,23 +109,23 @@ export default defineComponent({
}
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
onDragover(evt) {
const isFile = evt.dataTransfer.items[0].kind === 'file';
const isDriveFile = evt.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
e.preventDefault();
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
evt.preventDefault();
evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
}
},
onDrop(e): void {
onDrop(evt): void {
// ファイルだったら
if (e.dataTransfer.files.length == 1) {
e.preventDefault();
this.upload(e.dataTransfer.files[0]);
if (evt.dataTransfer.files.length === 1) {
evt.preventDefault();
this.upload(evt.dataTransfer.files[0]);
return;
} else if (e.dataTransfer.files.length > 1) {
e.preventDefault();
} else if (evt.dataTransfer.files.length > 1) {
evt.preventDefault();
os.alert({
type: 'error',
text: this.$ts.onlyOneFileCanBeAttached
@ -134,17 +134,17 @@ export default defineComponent({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const driveFile = evt.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
this.file = JSON.parse(driveFile);
e.preventDefault();
evt.preventDefault();
}
//#endregion
},
onKeydown(e) {
onKeydown(evt) {
this.typing();
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
if ((evt.which === 10 || evt.which === 13) && (evt.ctrlKey || evt.metaKey) && this.canSend) {
this.send();
}
},
@ -153,8 +153,8 @@ export default defineComponent({
this.typing();
},
chooseFile(e) {
selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => {
chooseFile(evt) {
selectFile(evt.currentTarget ?? evt.target, this.$ts.selectFile).then(file => {
this.file = file;
});
},
@ -164,7 +164,7 @@ export default defineComponent({
},
upload(file: File, name?: string) {
os.upload(file, this.$store.state.uploadFolder, name).then(res => {
uploadFile(file, this.$store.state.uploadFolder, name).then(res => {
this.file = res;
});
},
@ -192,25 +192,25 @@ export default defineComponent({
},
saveDraft() {
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
const drafts = JSON.parse(localStorage.getItem('message_drafts') || '{}');
data[this.draftKey] = {
drafts[this.draftKey] = {
updatedAt: new Date(),
data: {
text: this.text,
file: this.file
}
}
};
localStorage.setItem('message_drafts', JSON.stringify(data));
localStorage.setItem('message_drafts', JSON.stringify(drafts));
},
deleteDraft() {
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
const drafts = JSON.parse(localStorage.getItem('message_drafts') || '{}');
delete data[this.draftKey];
delete drafts[this.draftKey];
localStorage.setItem('message_drafts', JSON.stringify(data));
localStorage.setItem('message_drafts', JSON.stringify(drafts));
},
async insertEmoji(ev) {

View File

@ -166,23 +166,23 @@ const Component = defineComponent({
});
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
onDragover(evt) {
const isFile = evt.dataTransfer.items[0].kind === 'file';
const isDriveFile = evt.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
evt.dataTransfer.dropEffect = evt.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
evt.dataTransfer.dropEffect = 'none';
}
},
onDrop(e): void {
onDrop(evt): void {
// ファイルだったら
if (e.dataTransfer.files.length == 1) {
this.form.upload(e.dataTransfer.files[0]);
if (evt.dataTransfer.files.length === 1) {
this.form.upload(evt.dataTransfer.files[0]);
return;
} else if (e.dataTransfer.files.length > 1) {
} else if (evt.dataTransfer.files.length > 1) {
os.alert({
type: 'error',
text: this.$ts.onlyOneFileCanBeAttached
@ -191,8 +191,8 @@ const Component = defineComponent({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const driveFile = evt.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
this.form.file = file;
}
@ -209,7 +209,7 @@ const Component = defineComponent({
limit: max + 1,
untilId: this.existMoreMessages ? this.messages[0].id : undefined
}).then(messages => {
if (messages.length == max + 1) {
if (messages.length === max + 1) {
this.existMoreMessages = true;
messages.pop();
} else {
@ -235,7 +235,7 @@ const Component = defineComponent({
const _isBottom = isBottom(this.$el, 64);
this.messages.push(message);
if (message.userId != this.$i.id && !document.hidden) {
if (message.userId !== this.$i.id && !document.hidden) {
this.connection.send('read', {
id: message.id
});
@ -246,7 +246,7 @@ const Component = defineComponent({
this.$nextTick(() => {
this.scrollToBottom();
});
} else if (message.userId != this.$i.id) {
} else if (message.userId !== this.$i.id) {
// Notify
this.notifyNewMessage();
}
@ -256,7 +256,7 @@ const Component = defineComponent({
if (this.user) {
if (!Array.isArray(x)) x = [x];
for (const id of x) {
if (this.messages.some(x => x.id == id)) {
if (this.messages.some(x => x.id === id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist] = {
...this.messages[exist],
@ -266,7 +266,7 @@ const Component = defineComponent({
}
} else if (this.group) {
for (const id of x.ids) {
if (this.messages.some(x => x.id == id)) {
if (this.messages.some(x => x.id === id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist] = {
...this.messages[exist],

View File

@ -325,23 +325,23 @@ export default defineComponent({
preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
preview_quote: `> ${this.$ts._mfm.dummy}`,
preview_search: `${this.$ts._mfm.dummy} 検索`,
preview_jelly: `$[jelly 🍮]`,
preview_tada: `$[tada 🍮]`,
preview_jump: `$[jump 🍮]`,
preview_bounce: `$[bounce 🍮]`,
preview_shake: `$[shake 🍮]`,
preview_twitch: `$[twitch 🍮]`,
preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]`,
preview_jelly: `$[jelly 🍮] $[jelly.speed=5s 🍮]`,
preview_tada: `$[tada 🍮] $[tada.speed=5s 🍮]`,
preview_jump: `$[jump 🍮] $[jump.speed=5s 🍮]`,
preview_bounce: `$[bounce 🍮] $[bounce.speed=5s 🍮]`,
preview_shake: `$[shake 🍮] $[shake.speed=5s 🍮]`,
preview_twitch: `$[twitch 🍮] $[twitch.speed=5s 🍮]`,
preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]`,
preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`,
preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`,
preview_x2: `$[x2 🍮]`,
preview_x3: `$[x3 🍮]`,
preview_x4: `$[x4 🍮]`,
preview_blur: `$[blur ${this.$ts._mfm.dummy}]`,
preview_rainbow: `$[rainbow 🍮]`,
preview_rainbow: `$[rainbow 🍮] $[rainbow.speed=5s 🍮]`,
preview_sparkle: `$[sparkle 🍮]`,
preview_rotate: `$[rotate 🍮]`,
}
};
},
});
</script>

View File

@ -42,6 +42,7 @@ import MkSignin from '@/components/signin.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { login } from '@/account';
import { appendQuery, query } from '@/scripts/url';
export default defineComponent({
components: {
@ -82,7 +83,9 @@ export default defineComponent({
this.state = 'accepted';
if (this.callback) {
location.href = `${this.callback}?session=${this.session}`;
location.href = appendQuery(this.callback, query({
session: this.session
}));
}
},
deny() {

View File

@ -4,49 +4,34 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
<script lang="ts" setup>
import { watch } from 'vue';
import XAntenna from './editor.vue';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { MisskeyNavigator } from '@/scripts/navigate';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
XAntenna,
},
const nav = new MisskeyNavigator();
props: {
antennaId: {
type: String,
required: true,
}
},
let antenna: any = $ref(null);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.manageAntennas,
icon: 'fas fa-satellite',
},
antenna: null,
};
},
const props = defineProps<{
antennaId: string
}>();
watch: {
antennaId: {
async handler() {
this.antenna = await os.api('antennas/show', { antennaId: this.antennaId });
},
immediate: true,
}
},
function onAntennaUpdated() {
nav.push('/my/antennas');
}
methods: {
onAntennaUpdated() {
this.$router.push('/my/antennas');
},
os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => {
antenna = antennaResponse;
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
}
});
</script>

View File

@ -44,135 +44,100 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { watch } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkSelect from '@/components/form/select.vue';
import MkSwitch from '@/components/form/switch.vue';
import * as Acct from 'misskey-js/built/acct';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton, MkInput, MkTextarea, MkSelect, MkSwitch
},
const props = defineProps<{
antenna: any
}>();
props: {
antenna: {
type: Object,
required: true
}
},
const emit = defineEmits<{
(ev: 'created'): void,
(ev: 'updated'): void,
(ev: 'deleted'): void,
}>();
data() {
return {
name: '',
src: '',
userListId: null,
userGroupId: null,
users: '',
keywords: '',
excludeKeywords: '',
caseSensitive: false,
withReplies: false,
withFile: false,
notify: false,
userLists: null,
userGroups: null,
};
},
let name: string = $ref(props.antenna.name);
let src: string = $ref(props.antenna.src);
let userListId: any = $ref(props.antenna.userListId);
let userGroupId: any = $ref(props.antenna.userGroupId);
let users: string = $ref(props.antenna.users.join('\n'));
let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
let caseSensitive: boolean = $ref(props.antenna.caseSensitive);
let withReplies: boolean = $ref(props.antenna.withReplies);
let withFile: boolean = $ref(props.antenna.withFile);
let notify: boolean = $ref(props.antenna.notify);
let userLists: any = $ref(null);
let userGroups: any = $ref(null);
watch: {
async src() {
if (this.src === 'list' && this.userLists === null) {
this.userLists = await os.api('users/lists/list');
}
watch(() => src, async () => {
if (src === 'list' && userLists === null) {
userLists = await os.api('users/lists/list');
}
if (this.src === 'group' && this.userGroups === null) {
const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined');
if (src === 'group' && userGroups === null) {
const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined');
this.userGroups = [...groups1, ...groups2];
}
}
},
created() {
this.name = this.antenna.name;
this.src = this.antenna.src;
this.userListId = this.antenna.userListId;
this.userGroupId = this.antenna.userGroupId;
this.users = this.antenna.users.join('\n');
this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n');
this.caseSensitive = this.antenna.caseSensitive;
this.withReplies = this.antenna.withReplies;
this.withFile = this.antenna.withFile;
this.notify = this.antenna.notify;
},
methods: {
async saveAntenna() {
if (this.antenna.id == null) {
await os.apiWithDialog('antennas/create', {
name: this.name,
src: this.src,
userListId: this.userListId,
userGroupId: this.userGroupId,
withReplies: this.withReplies,
withFile: this.withFile,
notify: this.notify,
caseSensitive: this.caseSensitive,
users: this.users.trim().split('\n').map(x => x.trim()),
keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
});
this.$emit('created');
} else {
await os.apiWithDialog('antennas/update', {
antennaId: this.antenna.id,
name: this.name,
src: this.src,
userListId: this.userListId,
userGroupId: this.userGroupId,
withReplies: this.withReplies,
withFile: this.withFile,
notify: this.notify,
caseSensitive: this.caseSensitive,
users: this.users.trim().split('\n').map(x => x.trim()),
keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
});
this.$emit('updated');
}
},
async deleteAntenna() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.antenna.name }),
});
if (canceled) return;
await os.api('antennas/delete', {
antennaId: this.antenna.id,
});
os.success();
this.$emit('deleted');
},
addUser() {
os.selectUser().then(user => {
this.users = this.users.trim();
this.users += '\n@' + Acct.toString(user);
this.users = this.users.trim();
});
}
userGroups = [...groups1, ...groups2];
}
});
async function saveAntenna() {
const antennaData = {
name,
src,
userListId,
userGroupId,
withReplies,
withFile,
notify,
caseSensitive,
users: users.trim().split('\n').map(x => x.trim()),
keywords: keywords.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
};
if (props.antenna.id == null) {
await os.apiWithDialog('antennas/create', antennaData);
emit('created');
} else {
antennaData['antennaId'] = props.antenna.id;
await os.apiWithDialog('antennas/update', antennaData);
emit('updated');
}
}
async function deleteAntenna() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: props.antenna.name }),
});
if (canceled) return;
await os.api('antennas/delete', {
antennaId: props.antenna.id,
});
os.success();
emit('deleted');
}
function addUser() {
os.selectUser().then(user => {
users = users.trim();
users += '\n@' + Acct.toString(user as any);
users = users.trim();
});
}
</script>
<style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<MkSpacer :content-max="700">
<div class="ieepwinx">
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
<div class="">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
@ -14,35 +14,24 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkPagination,
MkButton,
},
const pagination = {
endpoint: 'antennas/list' as const,
limit: 10,
};
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
action: {
icon: 'fas fa-plus',
handler: this.create
}
},
pagination: {
endpoint: 'antennas/list' as const,
limit: 10,
},
};
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)'
}
});
</script>

View File

@ -108,6 +108,10 @@ export default defineComponent({
},
methods: {
fetch() {
this.hasPrev = false;
this.hasNext = false;
this.showPrev = false;
this.showNext = false;
this.note = null;
os.api('notes/show', {
noteId: this.noteId
@ -132,8 +136,8 @@ export default defineComponent({
this.hasPrev = prev.length !== 0;
this.hasNext = next.length !== 0;
});
}).catch(e => {
this.error = e;
}).catch(err => {
this.error = err;
});
}
}

View File

@ -114,7 +114,7 @@ export default defineComponent({
readonly: this.readonly,
getScriptBlockList: this.getScriptBlockList,
getPageBlockList: this.getPageBlockList
}
};
},
props: {

View File

@ -139,8 +139,8 @@ export default defineComponent({
username: this.username,
}).then(page => {
this.page = page;
}).catch(e => {
this.error = e;
}).catch(err => {
this.error = err;
});
},

View File

@ -12,7 +12,7 @@
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { defineAsyncComponent, onMounted } from 'vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
@ -36,7 +36,7 @@ async function save() {
onMounted(() => {
if (props.token == null) {
os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed');
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed');
router.push('/');
}
});

View File

@ -6,20 +6,20 @@
</div>
<MkContainer :foldable="true" class="_gap">
<template #header>{{ $ts.output }}</template>
<template #header>{{ i18n.ts.output }}</template>
<div class="bepmlvbi">
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
</div>
</MkContainer>
<div class="_gap">
{{ $ts.scratchpadDescription }}
{{ i18n.ts.scratchpadDescription }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, ref, watch } from 'vue';
import 'prismjs';
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike';
@ -27,103 +27,90 @@ import 'prismjs/components/prism-javascript';
import 'prismjs/themes/prism-okaidia.css';
import { PrismEditor } from 'vue-prism-editor';
import 'vue-prism-editor/dist/prismeditor.min.css';
import { AiScript, parse, utils, values } from '@syuilo/aiscript';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import MkContainer from '@/components/ui/container.vue';
import MkButton from '@/components/ui/button.vue';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkContainer,
MkButton,
PrismEditor,
},
const code = ref('');
const logs = ref<any[]>([]);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.scratchpad,
icon: 'fas fa-terminal',
},
code: '',
logs: [],
}
},
const saved = localStorage.getItem('scratchpad');
if (saved) {
code.value = saved;
}
watch: {
code() {
localStorage.setItem('scratchpad', this.code);
}
},
watch(code, () => {
localStorage.setItem('scratchpad', code.value);
});
created() {
const saved = localStorage.getItem('scratchpad');
if (saved) {
this.code = saved;
}
},
methods: {
async run() {
this.logs = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'scratchpad',
token: this.$i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
});
});
},
out: (value) => {
this.logs.push({
id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value),
print: true
});
},
log: (type, params) => {
switch (type) {
case 'end': this.logs.push({
id: Math.random(),
text: utils.valToString(params.val, true),
print: false
}); break;
default: break;
}
}
async function run() {
logs.value = [];
const aiscript = new AiScript(createAiScriptEnv({
storageKey: 'scratchpad',
token: $i?.token,
}), {
in: (q) => {
return new Promise(ok => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
});
});
let ast;
try {
ast = parse(this.code);
} catch (e) {
os.alert({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (e) {
os.alert({
type: 'error',
text: e
});
}
},
highlighter(code) {
return highlight(code, languages.js, 'javascript');
out: (value) => {
logs.value.push({
id: Math.random(),
text: value.type === 'str' ? value.value : utils.valToString(value),
print: true
});
},
log: (type, params) => {
switch (type) {
case 'end': logs.value.push({
id: Math.random(),
text: utils.valToString(params.val, true),
print: false
}); break;
default: break;
}
}
});
let ast;
try {
ast = parse(code.value);
} catch (error) {
os.alert({
type: 'error',
text: 'Syntax error :('
});
return;
}
try {
await aiscript.exec(ast);
} catch (error: any) {
os.alert({
type: 'error',
text: error.message
});
}
}
function highlighter(code) {
return highlight(code, languages.js, 'javascript');
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.scratchpad,
icon: 'fas fa-terminal',
},
});
</script>

View File

@ -1,49 +1,49 @@
<template>
<div>
<MkButton v-if="!data && !$i.twoFactorEnabled" @click="register">{{ $ts._2fa.registerDevice }}</MkButton>
<MkButton v-if="!twoFactorData && !$i.twoFactorEnabled" @click="register">{{ i18n.ts._2fa.registerDevice }}</MkButton>
<template v-if="$i.twoFactorEnabled">
<p>{{ $ts._2fa.alreadyRegistered }}</p>
<MkButton @click="unregister">{{ $ts.unregister }}</MkButton>
<p>{{ i18n.ts._2fa.alreadyRegistered }}</p>
<MkButton @click="unregister">{{ i18n.ts.unregister }}</MkButton>
<template v-if="supportsCredentials">
<hr class="totp-method-sep">
<h2 class="heading">{{ $ts.securityKey }}</h2>
<p>{{ $ts._2fa.securityKeyInfo }}</p>
<h2 class="heading">{{ i18n.ts.securityKey }}</h2>
<p>{{ i18n.ts._2fa.securityKeyInfo }}</p>
<div class="key-list">
<div v-for="key in $i.securityKeysList" class="key">
<h3>{{ key.name }}</h3>
<div class="last-used">{{ $ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
<MkButton @click="unregisterKey(key)">{{ $ts.unregister }}</MkButton>
<div class="last-used">{{ i18n.ts.lastUsed }}<MkTime :time="key.lastUsed"/></div>
<MkButton @click="unregisterKey(key)">{{ i18n.ts.unregister }}</MkButton>
</div>
</div>
<MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ $ts.passwordLessLogin }}</MkSwitch>
<MkSwitch v-if="$i.securityKeysList.length > 0" v-model="usePasswordLessLogin" @update:modelValue="updatePasswordLessLogin">{{ i18n.ts.passwordLessLogin }}</MkSwitch>
<MkInfo v-if="registration && registration.error" warn>{{ $ts.error }} {{ registration.error }}</MkInfo>
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $ts._2fa.registerKey }}</MkButton>
<MkInfo v-if="registration && registration.error" warn>{{ i18n.ts.error }} {{ registration.error }}</MkInfo>
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ i18n.ts._2fa.registerKey }}</MkButton>
<ol v-if="registration && !registration.error">
<li v-if="registration.stage >= 0">
{{ $ts.tapSecurityKey }}
{{ i18n.ts.tapSecurityKey }}
<i v-if="registration.saving && registration.stage == 0" class="fas fa-spinner fa-pulse fa-fw"></i>
</li>
<li v-if="registration.stage >= 1">
<MkForm :disabled="registration.stage != 1 || registration.saving">
<MkInput v-model="keyName" :max="30">
<template #label>{{ $ts.securityKeyName }}</template>
<template #label>{{ i18n.ts.securityKeyName }}</template>
</MkInput>
<MkButton :disabled="keyName.length == 0" @click="registerKey">{{ $ts.registerSecurityKey }}</MkButton>
<MkButton :disabled="keyName.length == 0" @click="registerKey">{{ i18n.ts.registerSecurityKey }}</MkButton>
<i v-if="registration.saving && registration.stage == 1" class="fas fa-spinner fa-pulse fa-fw"></i>
</MkForm>
</li>
</ol>
</template>
</template>
<div v-if="data && !$i.twoFactorEnabled">
<div v-if="twoFactorData && !$i.twoFactorEnabled">
<ol style="margin: 0; padding: 0 0 0 1em;">
<li>
<I18n :src="$ts._2fa.step1" tag="span">
<I18n :src="i18n.ts._2fa.step1" tag="span">
<template #a>
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
</template>
@ -52,19 +52,20 @@
</template>
</I18n>
</li>
<li>{{ $ts._2fa.step2 }}<br><img :src="data.qr"></li>
<li>{{ $ts._2fa.step3 }}<br>
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ $ts.token }}</template></MkInput>
<MkButton primary @click="submit">{{ $ts.done }}</MkButton>
<li>{{ i18n.ts._2fa.step2 }}<br><img :src="twoFactorData.qr"><p>{{ $ts._2fa.step2Url }}<br>{{ twoFactorData.url }}</p></li>
<li>
{{ i18n.ts._2fa.step3 }}<br>
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false"><template #label>{{ i18n.ts.token }}</template></MkInput>
<MkButton primary @click="submit">{{ i18n.ts.done }}</MkButton>
</li>
</ol>
<MkInfo>{{ $ts._2fa.step4 }}</MkInfo>
<MkInfo>{{ i18n.ts._2fa.step4 }}</MkInfo>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import { hostname } from '@/config';
import { byteify, hexify, stringify } from '@/scripts/2fa';
import MkButton from '@/components/ui/button.vue';
@ -72,155 +73,144 @@ import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/form/input.vue';
import MkSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton, MkInfo, MkInput, MkSwitch
},
const twoFactorData = ref<any>(null);
const supportsCredentials = ref(!!navigator.credentials);
const usePasswordLessLogin = ref($i!.usePasswordLessLogin);
const registration = ref<any>(null);
const keyName = ref('');
const token = ref(null);
data() {
return {
data: null,
supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$i.usePasswordLessLogin,
registration: null,
keyName: '',
token: null,
};
},
function register() {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/register', {
password: password
}).then(data => {
twoFactorData.value = data;
});
});
}
methods: {
register() {
os.inputText({
title: this.$ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/register', {
password: password
}).then(data => {
this.data = data;
});
function unregister() {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/unregister', {
password: password
}).then(() => {
usePasswordLessLogin.value = false;
updatePasswordLessLogin();
}).then(() => {
os.success();
$i!.twoFactorEnabled = false;
});
});
}
function submit() {
os.api('i/2fa/done', {
token: token.value
}).then(() => {
os.success();
$i!.twoFactorEnabled = true;
}).catch(err => {
os.alert({
type: 'error',
text: err,
});
});
}
function registerKey() {
registration.value.saving = true;
os.api('i/2fa/key-done', {
password: registration.value.password,
name: keyName.value,
challengeId: registration.value.challengeId,
// we convert each 16 bits to a string to serialise
clientDataJSON: stringify(registration.value.credential.response.clientDataJSON),
attestationObject: hexify(registration.value.credential.response.attestationObject)
}).then(key => {
registration.value = null;
key.lastUsed = new Date();
os.success();
});
}
function unregisterKey(key) {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
return os.api('i/2fa/remove-key', {
password,
credentialId: key.id
}).then(() => {
usePasswordLessLogin.value = false;
updatePasswordLessLogin();
}).then(() => {
os.success();
});
});
}
function addSecurityKey() {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/register-key', {
password
}).then(reg => {
registration.value = {
password,
challengeId: reg!.challengeId,
stage: 0,
publicKeyOptions: {
challenge: byteify(reg!.challenge, 'base64'),
rp: {
id: hostname,
name: 'Misskey'
},
user: {
id: byteify($i!.id, 'ascii'),
name: $i!.username,
displayName: $i!.name,
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
timeout: 60000,
attestation: 'direct'
},
saving: true
};
return navigator.credentials.create({
publicKey: registration.value.publicKeyOptions
});
},
}).then(credential => {
registration.value.credential = credential;
registration.value.saving = false;
registration.value.stage = 1;
}).catch(err => {
console.warn('Error while registering?', err);
registration.value.error = err.message;
registration.value.stage = -1;
});
});
}
unregister() {
os.inputText({
title: this.$ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/unregister', {
password: password
}).then(() => {
this.usePasswordLessLogin = false;
this.updatePasswordLessLogin();
}).then(() => {
os.success();
this.$i.twoFactorEnabled = false;
});
});
},
submit() {
os.api('i/2fa/done', {
token: this.token
}).then(() => {
os.success();
this.$i.twoFactorEnabled = true;
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
},
registerKey() {
this.registration.saving = true;
os.api('i/2fa/key-done', {
password: this.registration.password,
name: this.keyName,
challengeId: this.registration.challengeId,
// we convert each 16 bits to a string to serialise
clientDataJSON: stringify(this.registration.credential.response.clientDataJSON),
attestationObject: hexify(this.registration.credential.response.attestationObject)
}).then(key => {
this.registration = null;
key.lastUsed = new Date();
os.success();
})
},
unregisterKey(key) {
os.inputText({
title: this.$ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
return os.api('i/2fa/remove-key', {
password,
credentialId: key.id
}).then(() => {
this.usePasswordLessLogin = false;
this.updatePasswordLessLogin();
}).then(() => {
os.success();
});
});
},
addSecurityKey() {
os.inputText({
title: this.$ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/register-key', {
password
}).then(registration => {
this.registration = {
password,
challengeId: registration.challengeId,
stage: 0,
publicKeyOptions: {
challenge: byteify(registration.challenge, 'base64'),
rp: {
id: hostname,
name: 'Misskey'
},
user: {
id: byteify(this.$i.id, 'ascii'),
name: this.$i.username,
displayName: this.$i.name,
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
timeout: 60000,
attestation: 'direct'
},
saving: true
};
return navigator.credentials.create({
publicKey: this.registration.publicKeyOptions
});
}).then(credential => {
this.registration.credential = credential;
this.registration.saving = false;
this.registration.stage = 1;
}).catch(err => {
console.warn('Error while registering?', err);
this.registration.error = err.message;
this.registration.stage = -1;
});
});
},
updatePasswordLessLogin() {
os.api('i/2fa/password-less', {
value: !!this.usePasswordLessLogin
});
}
}
});
async function updatePasswordLessLogin() {
await os.api('i/2fa/password-less', {
value: !!usePasswordLessLogin.value
});
}
</script>

View File

@ -7,163 +7,150 @@
<FormSection>
<MkKeyValue>
<template #key>{{ $ts.registeredDate }}</template>
<template #key>{{ i18n.ts.registeredDate }}</template>
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
</MkKeyValue>
</FormSection>
<FormSection v-if="stats">
<template #label>{{ $ts.statistics }}</template>
<template #label>{{ i18n.ts.statistics }}</template>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.notesCount }}</template>
<template #key>{{ i18n.ts.notesCount }}</template>
<template #value>{{ number(stats.notesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.repliesCount }}</template>
<template #key>{{ i18n.ts.repliesCount }}</template>
<template #value>{{ number(stats.repliesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.renotesCount }}</template>
<template #key>{{ i18n.ts.renotesCount }}</template>
<template #value>{{ number(stats.renotesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.repliedCount }}</template>
<template #key>{{ i18n.ts.repliedCount }}</template>
<template #value>{{ number(stats.repliedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.renotedCount }}</template>
<template #key>{{ i18n.ts.renotedCount }}</template>
<template #value>{{ number(stats.renotedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pollVotesCount }}</template>
<template #key>{{ i18n.ts.pollVotesCount }}</template>
<template #value>{{ number(stats.pollVotesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pollVotedCount }}</template>
<template #key>{{ i18n.ts.pollVotedCount }}</template>
<template #value>{{ number(stats.pollVotedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.sentReactionsCount }}</template>
<template #key>{{ i18n.ts.sentReactionsCount }}</template>
<template #value>{{ number(stats.sentReactionsCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.receivedReactionsCount }}</template>
<template #key>{{ i18n.ts.receivedReactionsCount }}</template>
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.noteFavoritesCount }}</template>
<template #key>{{ i18n.ts.noteFavoritesCount }}</template>
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followingCount }}</template>
<template #key>{{ i18n.ts.followingCount }}</template>
<template #value>{{ number(stats.followingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template>
<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.local }})</template>
<template #value>{{ number(stats.localFollowingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template>
<template #key>{{ i18n.ts.followingCount }} ({{ i18n.ts.remote }})</template>
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followersCount }}</template>
<template #key>{{ i18n.ts.followersCount }}</template>
<template #value>{{ number(stats.followersCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template>
<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.local }})</template>
<template #value>{{ number(stats.localFollowersCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template>
<template #key>{{ i18n.ts.followersCount }} ({{ i18n.ts.remote }})</template>
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pageLikesCount }}</template>
<template #key>{{ i18n.ts.pageLikesCount }}</template>
<template #value>{{ number(stats.pageLikesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pageLikedCount }}</template>
<template #key>{{ i18n.ts.pageLikedCount }}</template>
<template #value>{{ number(stats.pageLikedCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.driveFilesCount }}</template>
<template #key>{{ i18n.ts.driveFilesCount }}</template>
<template #value>{{ number(stats.driveFilesCount) }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.driveUsage }}</template>
<template #key>{{ i18n.ts.driveUsage }}</template>
<template #value>{{ bytes(stats.driveUsage) }}</template>
</MkKeyValue>
</FormSection>
<FormSection>
<template #label>{{ $ts.other }}</template>
<template #label>{{ i18n.ts.other }}</template>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>emailVerified</template>
<template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template>
<template #value>{{ $i.emailVerified ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>twoFactorEnabled</template>
<template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template>
<template #value>{{ $i.twoFactorEnabled ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>securityKeys</template>
<template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template>
<template #value>{{ $i.securityKeys ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>usePasswordLessLogin</template>
<template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template>
<template #value>{{ $i.usePasswordLessLogin ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>isModerator</template>
<template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template>
<template #value>{{ $i.isModerator ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>isAdmin</template>
<template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template>
<template #value>{{ $i.isAdmin ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, onMounted, ref } from 'vue';
import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
MkKeyValue,
},
const stats = ref<any>({});
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.accountInfo,
icon: 'fas fa-info-circle'
},
stats: null
}
},
onMounted(() => {
os.api('users/stats', {
userId: $i!.id
}).then(response => {
stats.value = response;
});
});
mounted() {
os.api('users/stats', {
userId: this.$i.id
}).then(stats => {
this.stats = stats;
});
},
methods: {
number,
bytes,
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.accountInfo,
icon: 'fas fa-info-circle'
}
});
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="_formRoot">
<FormSuspense :p="init">
<FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
<FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ i18n.ts.addAccount }}</FormButton>
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
<div class="avatar">
@ -20,90 +20,89 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, defineExpose, ref } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { getAccounts, addAccount, login } from '@/account';
import { getAccounts, addAccount as addAccounts, login, $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSuspense,
FormButton,
},
const storedAccounts = ref<any>(null);
const accounts = ref<any>(null);
emits: ['info'],
const init = async () => {
getAccounts().then(accounts => {
storedAccounts.value = accounts.filter(x => x.id !== $i!.id);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.accounts,
icon: 'fas fa-users',
bg: 'var(--bg)',
},
storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)),
accounts: null,
init: async () => os.api('users/show', {
userIds: (await this.storedAccounts).map(x => x.id)
}).then(accounts => {
this.accounts = accounts;
}),
};
},
console.log(storedAccounts.value);
methods: {
menu(account, ev) {
os.popupMenu([{
text: this.$ts.switch,
icon: 'fas fa-exchange-alt',
action: () => this.switchAccount(account),
}, {
text: this.$ts.remove,
icon: 'fas fa-trash-alt',
danger: true,
action: () => this.removeAccount(account),
}], ev.currentTarget ?? ev.target);
return os.api('users/show', {
userIds: storedAccounts.value.map(x => x.id)
});
}).then(response => {
accounts.value = response;
console.log(accounts.value);
});
};
function menu(account, ev) {
os.popupMenu([{
text: i18n.ts.switch,
icon: 'fas fa-exchange-alt',
action: () => switchAccount(account),
}, {
text: i18n.ts.remove,
icon: 'fas fa-trash-alt',
danger: true,
action: () => removeAccount(account),
}], ev.currentTarget ?? ev.target);
}
function addAccount(ev) {
os.popupMenu([{
text: i18n.ts.existingAccount,
action: () => { addExistingAccount(); },
}, {
text: i18n.ts.createAccount,
action: () => { createAccount(); },
}], ev.currentTarget ?? ev.target);
}
function addExistingAccount() {
os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
done: res => {
addAccounts(res.id, res.i);
os.success();
},
}, 'closed');
}
addAccount(ev) {
os.popupMenu([{
text: this.$ts.existingAccount,
action: () => { this.addExistingAccount(); },
}, {
text: this.$ts.createAccount,
action: () => { this.createAccount(); },
}], ev.currentTarget ?? ev.target);
function createAccount() {
os.popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
done: res => {
addAccounts(res.id, res.i);
switchAccountWithToken(res.i);
},
}, 'closed');
}
addExistingAccount() {
os.popup(import('@/components/signin-dialog.vue'), {}, {
done: res => {
addAccount(res.id, res.i);
os.success();
},
}, 'closed');
},
async function switchAccount(account: any) {
const fetchedAccounts: any[] = await getAccounts();
const token = fetchedAccounts.find(x => x.id === account.id).token;
switchAccountWithToken(token);
}
createAccount() {
os.popup(import('@/components/signup-dialog.vue'), {}, {
done: res => {
addAccount(res.id, res.i);
this.switchAccountWithToken(res.i);
},
}, 'closed');
},
function switchAccountWithToken(token: string) {
login(token);
}
async switchAccount(account: any) {
const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token;
this.switchAccountWithToken(token);
},
switchAccountWithToken(token: string) {
login(token);
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.accounts,
icon: 'fas fa-users',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,56 +1,45 @@
<template>
<div class="_formRoot">
<FormButton primary class="_formBlock" @click="generateToken">{{ $ts.generateAccessToken }}</FormButton>
<FormLink to="/settings/apps" class="_formBlock">{{ $ts.manageAccessTokens }}</FormLink>
<FormButton primary class="_formBlock" @click="generateToken">{{ i18n.ts.generateAccessToken }}</FormButton>
<FormLink to="/settings/apps" class="_formBlock">{{ i18n.ts.manageAccessTokens }}</FormLink>
<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null" class="_formBlock">API console</FormLink>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, defineExpose, ref } from 'vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormButton,
FormLink,
},
const isDesktop = ref(window.innerWidth >= 1100);
emits: ['info'],
function generateToken() {
os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
data() {
return {
[symbols.PAGE_INFO]: {
title: 'API',
icon: 'fas fa-key',
bg: 'var(--bg)',
},
isDesktop: window.innerWidth >= 1100,
};
},
methods: {
generateToken() {
os.popup(import('@/components/token-generate-window.vue'), {}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
os.alert({
type: 'success',
title: this.$ts.token,
text: token
});
},
}, 'closed');
os.alert({
type: 'success',
title: i18n.ts.token,
text: token
});
},
}, 'closed');
}
defineExpose({
[symbols.PAGE_INFO]: {
title: 'API',
icon: 'fas fa-key',
bg: 'var(--bg)',
}
});
</script>

View File

@ -4,7 +4,7 @@
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $ts.nothing }}</div>
<div>{{ i18n.ts.nothing }}</div>
</div>
</template>
<template v-slot="{items}">
@ -14,18 +14,18 @@
<div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div>
<div class="_keyValue">
<div>{{ $ts.installedDate }}:</div>
<div>{{ i18n.ts.installedDate }}:</div>
<div><MkTime :time="token.createdAt"/></div>
</div>
<div class="_keyValue">
<div>{{ $ts.lastUsedDate }}:</div>
<div>{{ i18n.ts.lastUsedDate }}:</div>
<div><MkTime :time="token.lastUsedAt"/></div>
</div>
<div class="actions">
<button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button>
</div>
<details>
<summary>{{ $ts.details }}</summary>
<summary>{{ i18n.ts.details }}</summary>
<ul>
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
</ul>
@ -37,42 +37,34 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, ref } from 'vue';
import FormPagination from '@/components/ui/pagination.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormPagination,
},
const list = ref<any>(null);
emits: ['info'],
const pagination = {
endpoint: 'i/apps' as const,
limit: 100,
params: {
sort: '+lastUsedAt'
}
};
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.installedApps,
icon: 'fas fa-plug',
bg: 'var(--bg)',
},
pagination: {
endpoint: 'i/apps' as const,
limit: 100,
params: {
sort: '+lastUsedAt'
}
},
};
},
function revoke(token) {
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
list.value.reload();
});
}
methods: {
revoke(token) {
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
this.$refs.list.reload();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.installedApps,
icon: 'fas fa-plug',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="_formRoot">
<FormInfo warn class="_formBlock">{{ $ts.customCssWarn }}</FormInfo>
<FormInfo warn class="_formBlock">{{ i18n.ts.customCssWarn }}</FormInfo>
<FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;">
<template #label>CSS</template>
@ -8,50 +8,38 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormInfo from '@/components/ui/info.vue';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormTextarea,
FormInfo,
},
const localCustomCss = ref(localStorage.getItem('customCss') ?? '');
emits: ['info'],
async function apply() {
localStorage.setItem('customCss', localCustomCss.value);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.customCss,
icon: 'fas fa-code',
bg: 'var(--bg)',
},
localCustomCss: localStorage.getItem('customCss')
}
},
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
mounted() {
this.$watch('localCustomCss', this.apply);
},
unisonReload();
}
methods: {
async apply() {
localStorage.setItem('customCss', this.localCustomCss);
watch(localCustomCss, async () => {
await apply();
});
const { canceled } = await os.confirm({
type: 'info',
text: this.$ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.customCss,
icon: 'fas fa-code',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,36 +1,36 @@
<template>
<div class="_formRoot">
<FormGroup>
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
<FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
<FormSwitch v-model="navWindow">{{ i18n.ts.openInWindow }}</FormSwitch>
</FormGroup>
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
<FormRadios v-model="columnAlign" class="_formBlock">
<template #label>{{ $ts._deck.columnAlign }}</template>
<option value="left">{{ $ts.left }}</option>
<option value="center">{{ $ts.center }}</option>
<template #label>{{ i18n.ts._deck.columnAlign }}</template>
<option value="left">{{ i18n.ts.left }}</option>
<option value="center">{{ i18n.ts.center }}</option>
</FormRadios>
<FormRadios v-model="columnHeaderHeight" class="_formBlock">
<template #label>{{ $ts._deck.columnHeaderHeight }}</template>
<option :value="42">{{ $ts.narrow }}</option>
<option :value="45">{{ $ts.medium }}</option>
<option :value="48">{{ $ts.wide }}</option>
<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
<option :value="42">{{ i18n.ts.narrow }}</option>
<option :value="45">{{ i18n.ts.medium }}</option>
<option :value="48">{{ i18n.ts.wide }}</option>
</FormRadios>
<FormInput v-model="columnMargin" type="number" class="_formBlock">
<template #label>{{ $ts._deck.columnMargin }}</template>
<template #label>{{ i18n.ts._deck.columnMargin }}</template>
<template #suffix>px</template>
</FormInput>
<FormLink class="_formBlock" @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineExpose, watch } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormLink from '@/components/form/link.vue';
import FormRadios from '@/components/form/radios.vue';
@ -40,59 +40,41 @@ import { deckStore } from '@/ui/deck/deck-store';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormLink,
FormInput,
FormRadios,
FormGroup,
},
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
const profile = computed(deckStore.makeGetterSetter('profile'));
emits: ['info'],
watch(navWindow, async () => {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.deck,
icon: 'fas fa-columns',
bg: 'var(--bg)',
},
}
},
unisonReload();
});
computed: {
navWindow: deckStore.makeGetterSetter('navWindow'),
alwaysShowMainColumn: deckStore.makeGetterSetter('alwaysShowMainColumn'),
columnAlign: deckStore.makeGetterSetter('columnAlign'),
columnMargin: deckStore.makeGetterSetter('columnMargin'),
columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
profile: deckStore.makeGetterSetter('profile'),
},
async function setProfile() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
allowEmpty: false
});
if (canceled) return;
profile.value = name;
unisonReload();
}
watch: {
async navWindow() {
const { canceled } = await os.confirm({
type: 'info',
text: this.$ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
},
methods: {
async setProfile() {
const { canceled, result: name } = await os.inputText({
title: this.$ts._deck.profile,
allowEmpty: false
});
if (canceled) return;
this.profile = name;
unisonReload();
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.deck,
icon: 'fas fa-columns',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,64 +1,52 @@
<template>
<div class="_formRoot">
<FormInfo warn class="_formBlock">{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo class="_formBlock">{{ $ts._accountDelete.sendEmail }}</FormInfo>
<FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
<FormButton v-else disabled>{{ $ts._accountDelete.inProgress }}</FormButton>
<FormInfo warn class="_formBlock">{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo class="_formBlock">{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</FormButton>
<FormButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</FormButton>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose } from 'vue';
import FormInfo from '@/components/ui/info.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { signout } from '@/account';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormButton,
FormInfo,
},
async function deleteAccount() {
{
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.deleteAccountConfirm,
});
if (canceled) return;
}
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts._accountDelete.accountDelete,
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)',
},
}
},
const { canceled, result: password } = await os.inputText({
title: i18n.ts.password,
type: 'password'
});
if (canceled) return;
methods: {
async deleteAccount() {
{
const { canceled } = await os.confirm({
type: 'warning',
text: this.$ts.deleteAccountConfirm,
});
if (canceled) return;
}
await os.apiWithDialog('i/delete-account', {
password: password
});
const { canceled, result: password } = await os.inputText({
title: this.$ts.password,
type: 'password'
});
if (canceled) return;
await os.alert({
title: i18n.ts._accountDelete.started,
});
await os.apiWithDialog('i/delete-account', {
password: password
});
await signout();
}
await os.alert({
title: this.$ts._accountDelete.started,
});
signout();
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts._accountDelete.accountDelete,
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,41 +1,41 @@
<template>
<div class="_formRoot">
<FormSection v-if="!fetching">
<template #label>{{ $ts.usageAmount }}</template>
<template #label>{{ i18n.ts.usageAmount }}</template>
<div class="_formBlock uawsfosz">
<div class="meter"><div :style="meterStyle"></div></div>
</div>
<FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.capacity }}</template>
<template #key>{{ i18n.ts.capacity }}</template>
<template #value>{{ bytes(capacity, 1) }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.inUse }}</template>
<template #key>{{ i18n.ts.inUse }}</template>
<template #value>{{ bytes(usage, 1) }}</template>
</MkKeyValue>
</FormSplit>
</FormSection>
<FormSection>
<template #label>{{ $ts.statistics }}</template>
<template #label>{{ i18n.ts.statistics }}</template>
<MkChart src="per-user-drive" :args="{ user: $i }" span="day" :limit="7 * 5" :bar="true" :stacked="true" :detailed="false" :aspect-ratio="6"/>
</FormSection>
<FormSection>
<FormLink @click="chooseUploadFolder()">
{{ $ts.uploadFolder }}
{{ i18n.ts.uploadFolder }}
<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template>
<template #suffixIcon><i class="fas fa-folder-open"></i></template>
</FormLink>
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch>
<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ i18n.ts.keepOriginalUploading }}<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template></FormSwitch>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2';
<script lang="ts" setup>
import { computed, defineExpose, ref } from 'vue';
import tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue';
@ -46,80 +46,59 @@ import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
import MkChart from '@/components/chart.vue';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormLink,
FormSwitch,
FormSection,
MkKeyValue,
FormSplit,
MkChart,
},
const fetching = ref(true);
const usage = ref<any>(null);
const capacity = ref<any>(null);
const uploadFolder = ref<any>(null);
emits: ['info'],
const meterStyle = computed(() => {
return {
width: `${usage.value / capacity.value * 100}%`,
background: tinycolor({
h: 180 - (usage.value / capacity.value * 180),
s: 0.7,
l: 0.5
})
};
});
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.drive,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
},
fetching: true,
usage: null,
capacity: null,
uploadFolder: null,
const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
os.api('drive').then(info => {
capacity.value = info.capacity;
usage.value = info.usage;
fetching.value = false;
});
if (defaultStore.state.uploadFolder) {
os.api('drive/folders/show', {
folderId: defaultStore.state.uploadFolder
}).then(response => {
uploadFolder.value = response;
});
}
function chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
defaultStore.set('uploadFolder', folder ? folder.id : null);
os.success();
if (defaultStore.state.uploadFolder) {
uploadFolder.value = await os.api('drive/folders/show', {
folderId: defaultStore.state.uploadFolder
});
} else {
uploadFolder.value = 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
})
};
},
keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'),
},
async created() {
os.api('drive').then(info => {
this.capacity = info.capacity;
this.usage = info.usage;
this.fetching = false;
this.$nextTick(() => {
this.renderChart();
});
});
if (this.$store.state.uploadFolder) {
this.uploadFolder = await os.api('drive/folders/show', {
folderId: this.$store.state.uploadFolder
});
}
},
methods: {
chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
this.$store.set('uploadFolder', folder ? folder.id : null);
os.success();
if (this.$store.state.uploadFolder) {
this.uploadFolder = await os.api('drive/folders/show', {
folderId: this.$store.state.uploadFolder
});
} else {
this.uploadFolder = null;
}
});
},
bytes
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.drive,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
}
});
</script>

View File

@ -39,8 +39,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch } from 'vue';
<script lang="ts" setup>
import { defineExpose, onMounted, ref, watch } from 'vue';
import FormSection from '@/components/form/section.vue';
import FormInput from '@/components/form/input.vue';
import FormSwitch from '@/components/form/switch.vue';
@ -49,79 +49,62 @@ import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
FormSwitch,
FormInput,
},
const emailAddress = ref($i!.email);
emits: ['info'],
const onChangeReceiveAnnouncementEmail = (v) => {
os.api('i/update', {
receiveAnnouncementEmail: v
});
};
setup(props, context) {
const emailAddress = ref($i.email);
const INFO = {
title: i18n.ts.email,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
};
const onChangeReceiveAnnouncementEmail = (v) => {
os.api('i/update', {
receiveAnnouncementEmail: v
});
};
const saveEmailAddress = () => {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.apiWithDialog('i/update-email', {
password: password,
email: emailAddress.value,
});
});
};
const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention'));
const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply'));
const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote'));
const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow'));
const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest'));
const emailNotification_groupInvited = ref($i.emailNotificationTypes.includes('groupInvited'));
const saveNotificationSettings = () => {
os.api('i/update', {
emailNotificationTypes: [
...[emailNotification_mention.value ? 'mention' : null],
...[emailNotification_reply.value ? 'reply' : null],
...[emailNotification_quote.value ? 'quote' : null],
...[emailNotification_follow.value ? 'follow' : null],
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
...[emailNotification_groupInvited.value ? 'groupInvited' : null],
].filter(x => x != null)
});
};
watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => {
saveNotificationSettings();
const saveEmailAddress = () => {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.apiWithDialog('i/update-email', {
password: password,
email: emailAddress.value,
});
});
};
onMounted(() => {
watch(emailAddress, () => {
saveEmailAddress();
});
});
const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention'));
const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply'));
const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote'));
const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow'));
const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest'));
const emailNotification_groupInvited = ref($i!.emailNotificationTypes.includes('groupInvited'));
return {
[symbols.PAGE_INFO]: INFO,
emailAddress,
onChangeReceiveAnnouncementEmail,
emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited,
};
},
const saveNotificationSettings = () => {
os.api('i/update', {
emailNotificationTypes: [
...[emailNotification_mention.value ? 'mention' : null],
...[emailNotification_reply.value ? 'reply' : null],
...[emailNotification_quote.value ? 'quote' : null],
...[emailNotification_follow.value ? 'follow' : null],
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
...[emailNotification_groupInvited.value ? 'groupInvited' : null],
].filter(x => x != null)
});
};
watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => {
saveNotificationSettings();
});
onMounted(() => {
watch(emailAddress, () => {
saveEmailAddress();
});
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.email,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,10 +1,10 @@
<template>
<div class="_formRoot">
<FormSelect v-model="lang" class="_formBlock">
<template #label>{{ $ts.uiLanguage }}</template>
<template #label>{{ i18n.ts.uiLanguage }}</template>
<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
<template #caption>
<I18n :src="$ts.i18nInfo" tag="span">
<I18n :src="i18n.ts.i18nInfo" tag="span">
<template #link>
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
</template>
@ -13,48 +13,48 @@
</FormSelect>
<FormRadios v-model="overridedDeviceKind" class="_formBlock">
<template #label>{{ $ts.overridedDeviceKind }}</template>
<option :value="null">{{ $ts.auto }}</option>
<option value="smartphone"><i class="fas fa-mobile-alt"/> {{ $ts.smartphone }}</option>
<option value="tablet"><i class="fas fa-tablet-alt"/> {{ $ts.tablet }}</option>
<option value="desktop"><i class="fas fa-desktop"/> {{ $ts.desktop }}</option>
<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
<option :value="null">{{ i18n.ts.auto }}</option>
<option value="smartphone"><i class="fas fa-mobile-alt"/> {{ i18n.ts.smartphone }}</option>
<option value="tablet"><i class="fas fa-tablet-alt"/> {{ i18n.ts.tablet }}</option>
<option value="desktop"><i class="fas fa-desktop"/> {{ i18n.ts.desktop }}</option>
</FormRadios>
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ $ts.showFixedPostForm }}</FormSwitch>
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{ i18n.ts.showFixedPostForm }}</FormSwitch>
<FormSection>
<template #label>{{ $ts.behavior }}</template>
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ $ts.openImageInNewTab }}</FormSwitch>
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ $ts.enableInfiniteScroll }}</FormSwitch>
<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{ $ts.disablePagesScript }}</FormSwitch>
<template #label>{{ i18n.ts.behavior }}</template>
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ i18n.ts.openImageInNewTab }}</FormSwitch>
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch>
<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch>
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{ i18n.ts.disablePagesScript }}</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ $ts.whenServerDisconnected }}</template>
<option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option>
<option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option>
<option value="quiet">{{ $ts._serverDisconnectedBehavior.quiet }}</option>
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
</FormSelect>
</FormSection>
<FormSection>
<template #label>{{ $ts.appearance }}</template>
<FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ $ts.disableAnimatedMfm }}</FormSwitch>
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{ $ts.reduceUiAnimation }}</FormSwitch>
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ $ts.useBlurEffect }}</FormSwitch>
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ $ts.useBlurEffectForModal }}</FormSwitch>
<FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
<FormSwitch v-model="loadRawImages" class="_formBlock">{{ $ts.loadRawImages }}</FormSwitch>
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ $ts.squareAvatars }}</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ $ts.useSystemFont }}</FormSwitch>
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ $ts.useOsNativeEmojis }}
<template #label>{{ i18n.ts.appearance }}</template>
<FormSwitch v-model="disableAnimatedMfm" class="_formBlock">{{ i18n.ts.disableAnimatedMfm }}</FormSwitch>
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{ i18n.ts.reduceUiAnimation }}</FormSwitch>
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch>
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
<FormSwitch v-model="showGapBetweenNotesInTimeline" class="_formBlock">{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch>
<FormSwitch v-model="loadRawImages" class="_formBlock">{{ i18n.ts.loadRawImages }}</FormSwitch>
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch>
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ i18n.ts.useOsNativeEmojis }}
<div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</FormSwitch>
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ $ts.disableDrawer }}</FormSwitch>
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch>
<FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ $ts.fontSize }}</template>
<template #label>{{ i18n.ts.fontSize }}</template>
<option value="small"><span style="font-size: 14px;">Aa</span></option>
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
<option value="large"><span style="font-size: 18px;">Aa</span></option>
@ -63,36 +63,36 @@
</FormSection>
<FormSection>
<FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
<FormSwitch v-model="aiChanMode">{{ i18n.ts.aiChanMode }}</FormSwitch>
</FormSection>
<FormSelect v-model="instanceTicker" class="_formBlock">
<template #label>{{ $ts.instanceTicker }}</template>
<option value="none">{{ $ts._instanceTicker.none }}</option>
<option value="remote">{{ $ts._instanceTicker.remote }}</option>
<option value="always">{{ $ts._instanceTicker.always }}</option>
<template #label>{{ i18n.ts.instanceTicker }}</template>
<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
</FormSelect>
<FormSelect v-model="nsfw" class="_formBlock">
<template #label>{{ $ts.nsfw }}</template>
<option value="respect">{{ $ts._nsfw.respect }}</option>
<option value="ignore">{{ $ts._nsfw.ignore }}</option>
<option value="force">{{ $ts._nsfw.force }}</option>
<template #label>{{ i18n.ts.nsfw }}</template>
<option value="respect">{{ i18n.ts._nsfw.respect }}</option>
<option value="ignore">{{ i18n.ts._nsfw.ignore }}</option>
<option value="force">{{ i18n.ts._nsfw.force }}</option>
</FormSelect>
<FormGroup>
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
<template #label>{{ i18n.ts.defaultNavigationBehaviour }}</template>
<FormSwitch v-model="defaultSideView">{{ i18n.ts.openInSideView }}</FormSwitch>
</FormGroup>
<FormLink to="/settings/deck" class="_formBlock">{{ $ts.deck }}</FormLink>
<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink>
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineExpose, ref, watch } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
@ -102,122 +102,87 @@ import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/link.vue';
import { langs } from '@/config';
import { defaultStore } from '@/store';
import { ColdDeviceStorage } from '@/store';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkLink,
FormSwitch,
FormSelect,
FormRadios,
FormGroup,
FormLink,
FormSection,
},
const lang = ref(localStorage.getItem('lang'));
const fontSize = ref(localStorage.getItem('fontSize'));
const useSystemFont = ref(localStorage.getItem('useSystemFont') != null);
emits: ['info'],
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.general,
icon: 'fas fa-cogs',
bg: 'var(--bg)'
},
langs,
lang: localStorage.getItem('lang'),
fontSize: localStorage.getItem('fontSize'),
useSystemFont: localStorage.getItem('useSystemFont') != null,
}
},
unisonReload();
}
computed: {
overridedDeviceKind: defaultStore.makeGetterSetter('overridedDeviceKind'),
serverDisconnectedBehavior: defaultStore.makeGetterSetter('serverDisconnectedBehavior'),
reduceAnimation: defaultStore.makeGetterSetter('animation', v => !v, v => !v),
useBlurEffectForModal: defaultStore.makeGetterSetter('useBlurEffectForModal'),
useBlurEffect: defaultStore.makeGetterSetter('useBlurEffect'),
showGapBetweenNotesInTimeline: defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'),
disableAnimatedMfm: defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v),
useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'),
disableDrawer: defaultStore.makeGetterSetter('disableDrawer'),
disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'),
loadRawImages: defaultStore.makeGetterSetter('loadRawImages'),
imageNewTab: defaultStore.makeGetterSetter('imageNewTab'),
nsfw: defaultStore.makeGetterSetter('nsfw'),
disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'),
showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'),
defaultSideView: defaultStore.makeGetterSetter('defaultSideView'),
instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
squareAvatars: defaultStore.makeGetterSetter('squareAvatars'),
aiChanMode: defaultStore.makeGetterSetter('aiChanMode'),
},
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
const disableAnimatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v));
const useOsNativeEmojis = computed(defaultStore.makeGetterSetter('useOsNativeEmojis'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const disablePagesScript = computed(defaultStore.makeGetterSetter('disablePagesScript'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
const defaultSideView = computed(defaultStore.makeGetterSetter('defaultSideView'));
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode'));
watch: {
lang() {
localStorage.setItem('lang', this.lang);
localStorage.removeItem('locale');
this.reloadAsk();
},
watch(lang, () => {
localStorage.setItem('lang', lang.value as string);
localStorage.removeItem('locale');
});
fontSize() {
if (this.fontSize == null) {
localStorage.removeItem('fontSize');
} else {
localStorage.setItem('fontSize', this.fontSize);
}
this.reloadAsk();
},
watch(fontSize, () => {
if (fontSize.value == null) {
localStorage.removeItem('fontSize');
} else {
localStorage.setItem('fontSize', fontSize.value);
}
});
useSystemFont() {
if (this.useSystemFont) {
localStorage.setItem('useSystemFont', 't');
} else {
localStorage.removeItem('useSystemFont');
}
this.reloadAsk();
},
watch(useSystemFont, () => {
if (useSystemFont.value) {
localStorage.setItem('useSystemFont', 't');
} else {
localStorage.removeItem('useSystemFont');
}
});
enableInfiniteScroll() {
this.reloadAsk();
},
watch([
lang,
fontSize,
useSystemFont,
enableInfiniteScroll,
squareAvatars,
aiChanMode,
showGapBetweenNotesInTimeline,
instanceTicker,
overridedDeviceKind
], async () => {
await reloadAsk();
});
squareAvatars() {
this.reloadAsk();
},
aiChanMode() {
this.reloadAsk();
},
showGapBetweenNotesInTimeline() {
this.reloadAsk();
},
instanceTicker() {
this.reloadAsk();
},
overridedDeviceKind() {
this.reloadAsk();
},
},
methods: {
async reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: this.$ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.general,
icon: 'fas fa-cogs',
bg: 'var(--bg)'
}
});
</script>

View File

@ -37,8 +37,8 @@
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
<script lang="ts" setup>
import { defineExpose, ref } from 'vue';
import MkButton from '@/components/ui/button.vue';
import FormSection from '@/components/form/section.vue';
import FormGroup from '@/components/form/group.vue';
@ -48,108 +48,80 @@ import { selectFile } from '@/scripts/select-file';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
FormGroup,
FormSwitch,
MkButton,
},
const excludeMutingUsers = ref(false);
const excludeInactiveUsers = ref(false);
emits: ['info'],
const onExportSuccess = () => {
os.alert({
type: 'info',
text: i18n.ts.exportRequested,
});
};
setup(props, context) {
const INFO = {
title: i18n.ts.importAndExport,
icon: 'fas fa-boxes',
bg: 'var(--bg)',
};
const onImportSuccess = () => {
os.alert({
type: 'info',
text: i18n.ts.importRequested,
});
};
const excludeMutingUsers = ref(false);
const excludeInactiveUsers = ref(false);
const onError = (ev) => {
os.alert({
type: 'error',
text: ev.message,
});
};
const onExportSuccess = () => {
os.alert({
type: 'info',
text: i18n.ts.exportRequested,
});
};
const exportNotes = () => {
os.api('i/export-notes', {}).then(onExportSuccess).catch(onError);
};
const onImportSuccess = () => {
os.alert({
type: 'info',
text: i18n.ts.importRequested,
});
};
const exportFollowing = () => {
os.api('i/export-following', {
excludeMuting: excludeMutingUsers.value,
excludeInactive: excludeInactiveUsers.value,
})
.then(onExportSuccess).catch(onError);
};
const onError = (e) => {
os.alert({
type: 'error',
text: e.message,
});
};
const exportBlocking = () => {
os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError);
};
const exportNotes = () => {
os.api('i/export-notes', {}).then(onExportSuccess).catch(onError);
};
const exportUserLists = () => {
os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError);
};
const exportFollowing = () => {
os.api('i/export-following', {
excludeMuting: excludeMutingUsers.value,
excludeInactive: excludeInactiveUsers.value,
})
.then(onExportSuccess).catch(onError);
};
const exportMuting = () => {
os.api('i/export-mute', {}).then(onExportSuccess).catch(onError);
};
const exportBlocking = () => {
os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError);
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const exportUserLists = () => {
os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError);
};
const importUserLists = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const exportMuting = () => {
os.api('i/export-mute', {}).then(onExportSuccess).catch(onError);
};
const importMuting = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importBlocking = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importUserLists = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importMuting = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importBlocking = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
return {
[symbols.PAGE_INFO]: INFO,
excludeMutingUsers,
excludeInactiveUsers,
exportNotes,
exportFollowing,
exportBlocking,
exportUserLists,
exportMuting,
importFollowing,
importUserLists,
importMuting,
importBlocking,
};
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.importAndExport,
icon: 'fas fa-boxes',
bg: 'var(--bg)',
}
});
</script>

View File

@ -2,19 +2,22 @@
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<div class="header">
<div class="title">{{ $ts.settings }}</div>
<div class="title">
<MkA v-if="narrow" to="/settings">{{ $ts.settings }}</MkA>
<template v-else>{{ $ts.settings }}</template>
</div>
<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div>
</div>
<div class="body">
<div v-if="!narrow || page == null" class="nav">
<div v-if="!narrow || initialPage == null" class="nav">
<div class="baaadecd">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
</div>
</div>
<div class="main">
<div v-if="!(narrow && initialPage == null)" class="main">
<div class="bkzroven">
<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
</div>
</div>
</div>
@ -23,7 +26,7 @@
</template>
<script setup lang="ts">
import { computed, defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue';
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { i18n } from '@/i18n';
import MkInfo from '@/components/ui/info.vue';
import MkSuperMenu from '@/components/ui/super-menu.vue';
@ -33,6 +36,7 @@ import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { instance } from '@/instance';
import { $i } from '@/account';
import { MisskeyNavigator } from '@/scripts/navigate';
const props = defineProps<{
initialPage?: string
@ -45,53 +49,61 @@ const indexInfo = {
hideHeader: true,
};
const INFO = ref(indexInfo);
const page = ref(props.initialPage);
const narrow = ref(false);
const view = ref(null);
const el = ref<HTMLElement | null>(null);
const childInfo = ref(null);
const nav = new MisskeyNavigator();
const narrow = ref(false);
const NARROW_THRESHOLD = 600;
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = computed(() => [{
title: i18n.ts.basicSettings,
items: [{
icon: 'fas fa-user',
text: i18n.ts.profile,
to: '/settings/profile',
active: page.value === 'profile',
active: props.initialPage === 'profile',
}, {
icon: 'fas fa-lock-open',
text: i18n.ts.privacy,
to: '/settings/privacy',
active: page.value === 'privacy',
active: props.initialPage === 'privacy',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.reaction,
to: '/settings/reaction',
active: page.value === 'reaction',
active: props.initialPage === 'reaction',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.drive,
to: '/settings/drive',
active: page.value === 'drive',
active: props.initialPage === 'drive',
}, {
icon: 'fas fa-bell',
text: i18n.ts.notifications,
to: '/settings/notifications',
active: page.value === 'notifications',
active: props.initialPage === 'notifications',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.email,
to: '/settings/email',
active: page.value === 'email',
active: props.initialPage === 'email',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/settings/integration',
active: page.value === 'integration',
active: props.initialPage === 'integration',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/settings/security',
active: page.value === 'security',
active: props.initialPage === 'security',
}],
}, {
title: i18n.ts.clientSettings,
@ -99,27 +111,27 @@ const menuDef = computed(() => [{
icon: 'fas fa-cogs',
text: i18n.ts.general,
to: '/settings/general',
active: page.value === 'general',
active: props.initialPage === 'general',
}, {
icon: 'fas fa-palette',
text: i18n.ts.theme,
to: '/settings/theme',
active: page.value === 'theme',
active: props.initialPage === 'theme',
}, {
icon: 'fas fa-list-ul',
text: i18n.ts.menu,
to: '/settings/menu',
active: page.value === 'menu',
active: props.initialPage === 'menu',
}, {
icon: 'fas fa-music',
text: i18n.ts.sounds,
to: '/settings/sounds',
active: page.value === 'sounds',
active: props.initialPage === 'sounds',
}, {
icon: 'fas fa-plug',
text: i18n.ts.plugins,
to: '/settings/plugin',
active: page.value === 'plugin',
active: props.initialPage === 'plugin',
}],
}, {
title: i18n.ts.otherSettings,
@ -127,37 +139,37 @@ const menuDef = computed(() => [{
icon: 'fas fa-boxes',
text: i18n.ts.importAndExport,
to: '/settings/import-export',
active: page.value === 'import-export',
active: props.initialPage === 'import-export',
}, {
icon: 'fas fa-volume-mute',
text: i18n.ts.instanceMute,
to: '/settings/instance-mute',
active: page.value === 'instance-mute',
active: props.initialPage === 'instance-mute',
}, {
icon: 'fas fa-ban',
text: i18n.ts.muteAndBlock,
to: '/settings/mute-block',
active: page.value === 'mute-block',
active: props.initialPage === 'mute-block',
}, {
icon: 'fas fa-comment-slash',
text: i18n.ts.wordMute,
to: '/settings/word-mute',
active: page.value === 'word-mute',
active: props.initialPage === 'word-mute',
}, {
icon: 'fas fa-key',
text: 'API',
to: '/settings/api',
active: page.value === 'api',
active: props.initialPage === 'api',
}, {
icon: 'fas fa-bolt',
text: 'Webhook',
to: '/settings/webhook',
active: page.value === 'webhook',
active: props.initialPage === 'webhook',
}, {
icon: 'fas fa-ellipsis-h',
text: i18n.ts.other,
to: '/settings/other',
active: page.value === 'other',
active: props.initialPage === 'other',
}],
}, {
items: [{
@ -182,8 +194,8 @@ const menuDef = computed(() => [{
const pageProps = ref({});
const component = computed(() => {
if (page.value == null) return null;
switch (page.value) {
if (props.initialPage == null) return null;
switch (props.initialPage) {
case 'accounts': return defineAsyncComponent(() => import('./accounts.vue'));
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
@ -230,27 +242,41 @@ watch(component, () => {
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow.value) {
page.value = 'profile';
nav.push('/settings/profile');
} else {
page.value = props.initialPage;
if (props.initialPage == null) {
INFO.value = indexInfo;
}
}
});
onMounted(() => {
narrow.value = el.value.offsetWidth < 800;
if (!narrow.value) {
page.value = 'profile';
watch(narrow, () => {
if (props.initialPage == null && !narrow.value) {
nav.push('/settings/profile');
}
});
onMounted(() => {
ro.observe(el.value);
narrow.value = el.value.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow.value) {
nav.push('/settings/profile');
}
});
onUnmounted(() => {
ro.disconnect();
});
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
const pageChanged = (page) => {
if (page == null) return;
childInfo.value = page[symbols.PAGE_INFO];
if (page == null) {
childInfo.value = null;
} else {
childInfo.value = page[symbols.PAGE_INFO];
}
};
defineExpose({
@ -267,6 +293,7 @@ defineExpose({
font-weight: bold;
> .title {
display: block;
width: 34%;
}

View File

@ -1,67 +1,51 @@
<template>
<div class="_formRoot">
<MkInfo>{{ $ts._instanceMute.title }}</MkInfo>
<MkInfo>{{ i18n.ts._instanceMute.title }}</MkInfo>
<FormTextarea v-model="instanceMutes" class="_formBlock">
<template #label>{{ $ts._instanceMute.heading }}</template>
<template #caption>{{ $ts._instanceMute.instanceMuteDescription }}<br>{{ $ts._instanceMute.instanceMuteDescription2 }}</template>
<template #label>{{ i18n.ts._instanceMute.heading }}</template>
<template #caption>{{ i18n.ts._instanceMute.instanceMuteDescription }}<br>{{ i18n.ts._instanceMute.instanceMuteDescription2 }}</template>
</FormTextarea>
<MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script>
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import MkInfo from '@/components/ui/info.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
FormTextarea,
MkInfo,
},
const instanceMutes = ref($i!.mutedInstances.join('\n'));
const changed = ref(false);
emits: ['info'],
async function save() {
let mutes = instanceMutes.value
.trim().split('\n')
.map(el => el.trim())
.filter(el => el);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.instanceMute,
icon: 'fas fa-volume-mute'
},
tab: 'soft',
instanceMutes: '',
changed: false,
}
},
await os.api('i/update', {
mutedInstances: mutes,
});
watch: {
instanceMutes: {
handler() {
this.changed = true;
},
deep: true
},
},
changed.value = false;
async created() {
this.instanceMutes = this.$i.mutedInstances.join('\n');
},
// Refresh filtered list to signal to the user how they've been saved
instanceMutes.value = mutes.join('\n');
}
methods: {
async save() {
let mutes = this.instanceMutes.trim().split('\n').map(el => el.trim()).filter(el => el);
await os.api('i/update', {
mutedInstances: mutes,
});
this.changed = false;
watch(instanceMutes, () => {
changed.value = true;
});
// Refresh filtered list to signal to the user how they've been saved
this.instanceMutes = mutes.join('\n');
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.instanceMute,
icon: 'fas fa-volume-mute'
}
})
});
</script>

View File

@ -1,133 +1,98 @@
<template>
<div class="_formRoot">
<FormSection v-if="enableTwitterIntegration">
<FormSection v-if="instance.enableTwitterIntegration">
<template #label><i class="fab fa-twitter"></i> Twitter</template>
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton>
<p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ i18n.ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectTwitter">{{ i18n.ts.connectService }}</MkButton>
</FormSection>
<FormSection v-if="enableDiscordIntegration">
<FormSection v-if="instance.enableDiscordIntegration">
<template #label><i class="fab fa-discord"></i> Discord</template>
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton>
<p v-if="integrations.discord">{{ i18n.ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ i18n.ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectDiscord">{{ i18n.ts.connectService }}</MkButton>
</FormSection>
<FormSection v-if="enableGithubIntegration">
<FormSection v-if="instance.enableGithubIntegration">
<template #label><i class="fab fa-github"></i> GitHub</template>
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton>
<p v-if="integrations.github">{{ i18n.ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ i18n.ts.disconnectService }}</MkButton>
<MkButton v-else primary @click="connectGithub">{{ i18n.ts.connectService }}</MkButton>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineExpose, onMounted, ref, watch } from 'vue';
import { apiUrl } from '@/config';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
MkButton
},
const twitterForm = ref<Window | null>(null);
const discordForm = ref<Window | null>(null);
const githubForm = ref<Window | null>(null);
emits: ['info'],
const integrations = computed(() => $i!.integrations);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
},
apiUrl,
twitterForm: null,
discordForm: null,
githubForm: null,
enableTwitterIntegration: false,
enableDiscordIntegration: false,
enableGithubIntegration: false,
};
},
function openWindow(service: string, type: string) {
return window.open(`${apiUrl}/${type}/${service}`,
`${service}_${type}_window`,
'height=570, width=520'
);
}
computed: {
integrations() {
return this.$i.integrations;
},
meta() {
return this.$instance;
},
},
function connectTwitter() {
twitterForm.value = openWindow('twitter', 'connect');
}
created() {
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
},
function disconnectTwitter() {
openWindow('twitter', 'disconnect');
}
mounted() {
document.cookie = `igi=${this.$i.token}; path=/;` +
` max-age=31536000;` +
(document.location.protocol.startsWith('https') ? ' secure' : '');
function connectDiscord() {
discordForm.value = openWindow('discord', 'connect');
}
this.$watch('integrations', () => {
if (this.integrations.twitter) {
if (this.twitterForm) this.twitterForm.close();
}
if (this.integrations.discord) {
if (this.discordForm) this.discordForm.close();
}
if (this.integrations.github) {
if (this.githubForm) this.githubForm.close();
}
}, {
deep: true
});
},
function disconnectDiscord() {
openWindow('discord', 'disconnect');
}
methods: {
connectTwitter() {
this.twitterForm = window.open(apiUrl + '/connect/twitter',
'twitter_connect_window',
'height=570, width=520');
},
function connectGithub() {
githubForm.value = openWindow('github', 'connect');
}
disconnectTwitter() {
window.open(apiUrl + '/disconnect/twitter',
'twitter_disconnect_window',
'height=570, width=520');
},
function disconnectGithub() {
openWindow('github', 'disconnect');
}
connectDiscord() {
this.discordForm = window.open(apiUrl + '/connect/discord',
'discord_connect_window',
'height=570, width=520');
},
onMounted(() => {
document.cookie = `igi=${$i!.token}; path=/;` +
` max-age=31536000;` +
(document.location.protocol.startsWith('https') ? ' secure' : '');
disconnectDiscord() {
window.open(apiUrl + '/disconnect/discord',
'discord_disconnect_window',
'height=570, width=520');
},
watch(integrations, () => {
if (integrations.value.twitter) {
if (twitterForm.value) twitterForm.value.close();
}
if (integrations.value.discord) {
if (discordForm.value) discordForm.value.close();
}
if (integrations.value.github) {
if (githubForm.value) githubForm.value.close();
}
});
});
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');
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,24 +1,24 @@
<template>
<div class="_formRoot">
<FormTextarea v-model="items" tall manual-save class="_formBlock">
<template #label>{{ $ts.menu }}</template>
<template #caption><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template>
<template #label>{{ i18n.ts.menu }}</template>
<template #caption><button class="_textButton" @click="addItem">{{ i18n.ts.addItem }}</button></template>
</FormTextarea>
<FormRadios v-model="menuDisplay" class="_formBlock">
<template #label>{{ $ts.display }}</template>
<option value="sideFull">{{ $ts._menuDisplay.sideFull }}</option>
<option value="sideIcon">{{ $ts._menuDisplay.sideIcon }}</option>
<option value="top">{{ $ts._menuDisplay.top }}</option>
<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ $ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
<template #label>{{ i18n.ts.display }}</template>
<option value="sideFull">{{ i18n.ts._menuDisplay.sideFull }}</option>
<option value="sideIcon">{{ i18n.ts._menuDisplay.sideIcon }}</option>
<option value="top">{{ i18n.ts._menuDisplay.top }}</option>
<!-- <MkRadio v-model="menuDisplay" value="hide" disabled>{{ i18n.ts._menuDisplay.hide }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
</FormRadios>
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ i18n.ts.default }}</FormButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineExpose, ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormButton from '@/components/ui/button.vue';
@ -27,81 +27,60 @@ import { menuDef } from '@/menu';
import { defaultStore } from '@/store';
import * as symbols from '@/symbols';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormButton,
FormTextarea,
FormRadios,
},
const items = ref(defaultStore.state.menu.join('\n'));
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.menu,
icon: 'fas fa-list-ul',
bg: 'var(--bg)',
},
menuDef: menuDef,
items: defaultStore.state.menu.join('\n'),
}
},
const split = computed(() => items.value.trim().split('\n').filter(x => x.trim() !== ''));
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
computed: {
splited(): string[] {
return this.items.trim().split('\n').filter(x => x.trim() !== '');
},
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting
});
if (canceled) return;
menuDisplay: defaultStore.makeGetterSetter('menuDisplay')
},
unisonReload();
}
watch: {
menuDisplay() {
this.reloadAsk();
},
async function addItem() {
const menu = Object.keys(menuDef).filter(k => !defaultStore.state.menu.includes(k));
const { canceled, result: item } = await os.select({
title: i18n.ts.addItem,
items: [...menu.map(k => ({
value: k, text: i18n.ts[menuDef[k].title]
})), {
value: '-', text: i18n.ts.divider
}]
});
if (canceled) return;
items.value = [...split.value, item].join('\n');
}
items() {
this.save();
},
},
async function save() {
defaultStore.set('menu', split.value);
await reloadAsk();
}
methods: {
async addItem() {
const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
const { canceled, result: item } = await os.select({
title: this.$ts.addItem,
items: [...menu.map(k => ({
value: k, text: this.$ts[this.menuDef[k].title]
})), ...[{
value: '-', text: this.$ts.divider
}]]
});
if (canceled) return;
this.items = [...this.splited, item].join('\n');
},
function reset() {
defaultStore.reset('menu');
items.value = defaultStore.state.menu.join('\n');
}
save() {
this.$store.set('menu', this.splited);
this.reloadAsk();
},
watch(items, async () => {
await save();
});
reset() {
this.$store.reset('menu');
this.items = this.$store.state.menu.join('\n');
},
watch(menuDisplay, async () => {
await reloadAsk();
});
async reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: this.$ts.reloadToApplySetting,
showCancelButton: true
});
if (canceled) return;
unisonReload();
}
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.menu,
icon: 'fas fa-list-ul',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,71 +1,59 @@
<template>
<div class="_formRoot">
<FormLink class="_formBlock" @click="configure"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.notificationSetting }}</FormLink>
<FormLink class="_formBlock" @click="configure"><template #icon><i class="fas fa-cog"></i></template>{{ i18n.ts.notificationSetting }}</FormLink>
<FormSection>
<FormLink class="_formBlock" @click="readAllNotifications">{{ $ts.markAsReadAllNotifications }}</FormLink>
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ $ts.markAsReadAllUnreadNotes }}</FormLink>
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ $ts.markAsReadAllTalkMessages }}</FormLink>
<FormLink class="_formBlock" @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, defineExpose } from 'vue';
import FormButton from '@/components/ui/button.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import { notificationTypes } from 'misskey-js';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormLink,
FormButton,
FormSection,
},
async function readAllUnreadNotes() {
await os.api('i/read-all-unread-notes');
}
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.notifications,
icon: 'fas fa-bell',
bg: 'var(--bg)',
},
async function readAllMessagingMessages() {
await os.api('i/read-all-messaging-messages');
}
async function readAllNotifications() {
await os.api('notifications/mark-all-as-read');
}
function configure() {
const includingTypes = notificationTypes.filter(x => !$i!.mutingNotificationTypes.includes(x));
os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), {
includingTypes,
showGlobalToggle: false,
}, {
done: async (res) => {
const { includingTypes: value } = res;
await os.apiWithDialog('i/update', {
mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
}).then(i => {
$i!.mutingNotificationTypes = i.mutingNotificationTypes;
});
}
},
}, 'closed');
}
methods: {
readAllUnreadNotes() {
os.api('i/read-all-unread-notes');
},
readAllMessagingMessages() {
os.api('i/read-all-messaging-messages');
},
readAllNotifications() {
os.api('notifications/mark-all-as-read');
},
configure() {
const includingTypes = notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
os.popup(import('@/components/notification-setting-window.vue'), {
includingTypes,
showGlobalToggle: false,
}, {
done: async (res) => {
const { includingTypes: value } = res;
await os.apiWithDialog('i/update', {
mutingNotificationTypes: notificationTypes.filter(x => !value.includes(x)),
}).then(i => {
this.$i.mutingNotificationTypes = i.mutingNotificationTypes;
});
}
}, 'closed');
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.notifications,
icon: 'fas fa-bell',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,66 +1,44 @@
<template>
<div class="_formRoot">
<FormSwitch :value="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote">
{{ $ts.showFeaturedNotesInTimeline }}
<FormSwitch v-model="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote">
{{ i18n.ts.showFeaturedNotesInTimeline }}
</FormSwitch>
<!--
<FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #caption>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
<FormSwitch v-model="reportError" class="_formBlock">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></FormSwitch>
-->
<FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink>
<FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink>
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineExpose } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import * as os from '@/os';
import { debug } from '@/config';
import { defaultStore } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
FormSwitch,
FormLink,
},
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.other,
icon: 'fas fa-ellipsis-h',
bg: 'var(--bg)',
},
debug,
}
},
function onChangeInjectFeaturedNote(v) {
os.api('i/update', {
injectFeaturedNote: v
}).then((i) => {
$i!.injectFeaturedNote = i.injectFeaturedNote;
});
}
computed: {
reportError: defaultStore.makeGetterSetter('reportError'),
},
methods: {
changeDebug(v) {
console.log(v);
localStorage.setItem('debug', v.toString());
unisonReload();
},
onChangeInjectFeaturedNote(v) {
os.api('i/update', {
injectFeaturedNote: v
});
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.other,
icon: 'fas fa-ellipsis-h',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,19 +1,19 @@
<template>
<div class="_formRoot">
<FormInfo warn class="_formBlock">{{ $ts._plugin.installWarn }}</FormInfo>
<FormInfo warn class="_formBlock">{{ i18n.ts._plugin.installWarn }}</FormInfo>
<FormTextarea v-model="code" tall class="_formBlock">
<template #label>{{ $ts.code }}</template>
<template #label>{{ i18n.ts.code }}</template>
</FormTextarea>
<div class="_formBlock">
<FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
<FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ i18n.ts.install }}</FormButton>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, defineAsyncComponent, nextTick, ref } from 'vue';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
@ -23,111 +23,101 @@ import FormInfo from '@/components/ui/info.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
FormTextarea,
FormButton,
FormInfo,
},
const code = ref(null);
emits: ['info'],
function installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
}));
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts._plugin.install,
icon: 'fas fa-download',
bg: 'var(--bg)',
},
code: null,
}
},
async function install() {
let ast;
try {
ast = parse(code.value);
} catch (err) {
os.alert({
type: 'error',
text: 'Syntax error :('
});
return;
}
methods: {
installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
}));
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.alert({
type: 'error',
text: 'No metadata found :('
});
return;
}
const metadata = meta.get(null);
if (metadata == null) {
os.alert({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = metadata;
if (name == null || version == null || author == null) {
os.alert({
type: 'error',
text: 'Required property not found :('
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(defineAsyncComponent(() => import('@/components/token-generate-window.vue')), {
title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription,
initialName: name,
initialPermissions: permissions
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
}
}, 'closed');
});
installPlugin({
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
async install() {
let ast;
try {
ast = parse(this.code);
} catch (e) {
os.alert({
type: 'error',
text: 'Syntax error :('
});
return;
}
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.alert({
type: 'error',
text: 'No metadata found :('
});
return;
}
const data = meta.get(null);
if (data == null) {
os.alert({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
os.alert({
type: 'error',
text: 'Required property not found :('
});
return;
}
os.success();
const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(import('@/components/token-generate-window.vue'), {
title: this.$ts.tokenRequested,
information: this.$ts.pluginTokenRequestedDescription,
initialName: name,
initialPermissions: permissions
}, {
done: async result => {
const { name, permissions } = result;
const { token } = await os.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
nextTick(() => {
unisonReload();
});
}
res(token);
}
}, 'closed');
});
this.installPlugin({
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
os.success();
this.$nextTick(() => {
unisonReload();
});
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts._plugin.install,
icon: 'fas fa-download',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,38 +1,38 @@
<template>
<div class="_formRoot">
<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink>
<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
<FormSection>
<template #label>{{ $ts.manage }}</template>
<template #label>{{ i18n.ts.manage }}</template>
<div v-for="plugin in plugins" :key="plugin.id" class="_formBlock _panel" style="padding: 20px;">
<span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
<FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
<FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</FormSwitch>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.author }}</template>
<template #key>{{ i18n.ts.author }}</template>
<template #value>{{ plugin.author }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.description }}</template>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{ plugin.description }}</template>
</MkKeyValue>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.permission }}</template>
<template #key>{{ i18n.ts.permission }}</template>
<template #value>{{ plugin.permission }}</template>
</MkKeyValue>
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
<MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ i18n.ts.settings }}</MkButton>
<MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.uninstall }}</MkButton>
</div>
</div>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, nextTick, ref } from 'vue';
import FormLink from '@/components/form/link.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSection from '@/components/form/section.vue';
@ -41,67 +41,54 @@ import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import * as symbols from '@/symbols';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormLink,
FormSwitch,
FormSection,
MkButton,
MkKeyValue,
},
const plugins = ref(ColdDeviceStorage.get('plugins'));
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.plugins,
icon: 'fas fa-plug',
bg: 'var(--bg)',
},
plugins: ColdDeviceStorage.get('plugins'),
}
},
function uninstall(plugin) {
ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id));
os.success();
nextTick(() => {
unisonReload();
});
}
methods: {
uninstall(plugin) {
ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
os.success();
this.$nextTick(() => {
unisonReload();
});
},
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async function config(plugin) {
const config = plugin.config;
for (const key in plugin.configData) {
config[key].default = plugin.configData[key];
}
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async config(plugin) {
const config = plugin.config;
for (const key in plugin.configData) {
config[key].default = plugin.configData[key];
}
const { canceled, result } = await os.form(plugin.name, config);
if (canceled) return;
const { canceled, result } = await os.form(plugin.name, config);
if (canceled) return;
const coldPlugins = ColdDeviceStorage.get('plugins');
coldPlugins.find(p => p.id === plugin.id)!.configData = result;
ColdDeviceStorage.set('plugins', coldPlugins);
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).configData = result;
ColdDeviceStorage.set('plugins', plugins);
nextTick(() => {
location.reload();
});
}
this.$nextTick(() => {
location.reload();
});
},
function changeActive(plugin, active) {
const coldPlugins = ColdDeviceStorage.get('plugins');
coldPlugins.find(p => p.id === plugin.id)!.active = active;
ColdDeviceStorage.set('plugins', coldPlugins);
changeActive(plugin, active) {
const plugins = ColdDeviceStorage.get('plugins');
plugins.find(p => p.id === plugin.id).active = active;
ColdDeviceStorage.set('plugins', plugins);
nextTick(() => {
location.reload();
});
}
this.$nextTick(() => {
location.reload();
});
}
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.plugins,
icon: 'fas fa-plug',
bg: 'var(--bg)',
}
});
</script>

View File

@ -62,7 +62,7 @@
</template>
<script lang="ts" setup>
import { defineComponent, reactive, watch } from 'vue';
import { reactive, watch } from 'vue';
import MkButton from '@/components/ui/button.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@ -132,8 +132,21 @@ function save() {
function changeAvatar(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => {
let originalOrCropped = file;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.t('cropImageAsk'),
});
if (!canceled) {
originalOrCropped = await os.cropImage(file, {
aspectRatio: 1,
});
}
const i = await os.apiWithDialog('i/update', {
avatarId: file.id,
avatarId: originalOrCropped.id,
});
$i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl;
@ -142,8 +155,21 @@ function changeAvatar(ev) {
function changeBanner(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
let originalOrCropped = file;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.t('cropImageAsk'),
});
if (!canceled) {
originalOrCropped = await os.cropImage(file, {
aspectRatio: 2,
});
}
const i = await os.apiWithDialog('i/update', {
bannerId: file.id,
bannerId: originalOrCropped.id,
});
$i.bannerId = i.bannerId;
$i.bannerUrl = i.bannerUrl;

View File

@ -54,7 +54,7 @@
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { defineAsyncComponent, watch } from 'vue';
import XDraggable from 'vuedraggable';
import FormInput from '@/components/form/input.vue';
import FormRadios from '@/components/form/radios.vue';
@ -88,7 +88,7 @@ function remove(reaction, ev: MouseEvent) {
}
function preview(ev: MouseEvent) {
os.popup(import('@/components/emoji-picker-dialog.vue'), {
os.popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
asReactionPicker: true,
src: ev.currentTarget ?? ev.target,
}, {}, 'closed');

View File

@ -1,17 +1,17 @@
<template>
<div class="_formRoot">
<FormSection>
<template #label>{{ $ts.password }}</template>
<FormButton primary @click="change()">{{ $ts.changePassword }}</FormButton>
<template #label>{{ i18n.ts.password }}</template>
<FormButton primary @click="change()">{{ i18n.ts.changePassword }}</FormButton>
</FormSection>
<FormSection>
<template #label>{{ $ts.twoStepAuthentication }}</template>
<template #label>{{ i18n.ts.twoStepAuthentication }}</template>
<X2fa/>
</FormSection>
<FormSection>
<template #label>{{ $ts.signinHistory }}</template>
<template #label>{{ i18n.ts.signinHistory }}</template>
<MkPagination :pagination="pagination">
<template v-slot="{items}">
<div>
@ -30,15 +30,15 @@
<FormSection>
<FormSlot>
<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ $ts.regenerateLoginToken }}</FormButton>
<template #caption>{{ $ts.regenerateLoginTokenDescription }}</template>
<FormButton danger @click="regenerateToken"><i class="fas fa-sync-alt"></i> {{ i18n.ts.regenerateLoginToken }}</FormButton>
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
</FormSlot>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose } from 'vue';
import FormSection from '@/components/form/section.vue';
import FormSlot from '@/components/form/slot.vue';
import FormButton from '@/components/ui/button.vue';
@ -46,77 +46,63 @@ import MkPagination from '@/components/ui/pagination.vue';
import X2fa from './2fa.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
FormButton,
MkPagination,
FormSlot,
X2fa,
},
const pagination = {
endpoint: 'i/signin-history' as const,
limit: 5,
};
async function change() {
const { canceled: canceled1, result: currentPassword } = await os.inputText({
title: i18n.ts.currentPassword,
type: 'password'
});
if (canceled1) return;
const { canceled: canceled2, result: newPassword } = await os.inputText({
title: i18n.ts.newPassword,
type: 'password'
});
if (canceled2) return;
const { canceled: canceled3, result: newPassword2 } = await os.inputText({
title: i18n.ts.newPasswordRetype,
type: 'password'
});
if (canceled3) return;
if (newPassword !== newPassword2) {
os.alert({
type: 'error',
text: i18n.ts.retypedNotMatch
});
return;
}
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
},
pagination: {
endpoint: 'i/signin-history' as const,
limit: 5,
},
}
},
os.apiWithDialog('i/change-password', {
currentPassword,
newPassword
});
}
methods: {
async change() {
const { canceled: canceled1, result: currentPassword } = await os.inputText({
title: this.$ts.currentPassword,
type: 'password'
});
if (canceled1) return;
function regenerateToken() {
os.inputText({
title: i18n.ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/regenerate_token', {
password: password
});
});
}
const { canceled: canceled2, result: newPassword } = await os.inputText({
title: this.$ts.newPassword,
type: 'password'
});
if (canceled2) return;
const { canceled: canceled3, result: newPassword2 } = await os.inputText({
title: this.$ts.newPasswordRetype,
type: 'password'
});
if (canceled3) return;
if (newPassword !== newPassword2) {
os.alert({
type: 'error',
text: this.$ts.retypedNotMatch
});
return;
}
os.apiWithDialog('i/change-password', {
currentPassword,
newPassword
});
},
regenerateToken() {
os.inputText({
title: this.$ts.password,
type: 'password'
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/regenerate_token', {
password: password
});
});
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
}
});
</script>

View File

@ -1,24 +1,24 @@
<template>
<div class="_formRoot">
<FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :text-converter="(v) => `${Math.floor(v * 100)}%`" class="_formBlock">
<template #label>{{ $ts.masterVolume }}</template>
<template #label>{{ i18n.ts.masterVolume }}</template>
</FormRange>
<FormSection>
<template #label>{{ $ts.sounds }}</template>
<template #label>{{ i18n.ts.sounds }}</template>
<FormLink v-for="type in Object.keys(sounds)" :key="type" style="margin-bottom: 8px;" @click="edit(type)">
{{ $t('_sfx.' + type) }}
<template #suffix>{{ sounds[type].type || $ts.none }}</template>
<template #suffix>{{ sounds[type].type || i18n.ts.none }}</template>
<template #suffixIcon><i class="fas fa-chevron-down"></i></template>
</FormLink>
</FormSection>
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ $ts.default }}</FormButton>
<FormButton danger class="_formBlock" @click="reset()"><i class="fas fa-redo"></i> {{ i18n.ts.default }}</FormButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, defineExpose, ref } from 'vue';
import FormRange from '@/components/form/range.vue';
import FormButton from '@/components/ui/button.vue';
import FormLink from '@/components/form/link.vue';
@ -27,6 +27,28 @@ import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import { playFile } from '@/scripts/sound';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
const masterVolume = computed({
get: () => {
return ColdDeviceStorage.get('sound_masterVolume');
},
set: (value) => {
ColdDeviceStorage.set('sound_masterVolume', value);
}
});
const volumeIcon = computed(() => masterVolume.value === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up');
const sounds = ref({
note: ColdDeviceStorage.get('sound_note'),
noteMy: ColdDeviceStorage.get('sound_noteMy'),
notification: ColdDeviceStorage.get('sound_notification'),
chat: ColdDeviceStorage.get('sound_chat'),
chatBg: ColdDeviceStorage.get('sound_chatBg'),
antenna: ColdDeviceStorage.get('sound_antenna'),
channel: ColdDeviceStorage.get('sound_channel'),
});
const soundsTypes = [
null,
@ -55,94 +77,58 @@ const soundsTypes = [
'noizenecio/kick_gaba2',
];
export default defineComponent({
components: {
FormLink,
FormButton,
FormRange,
FormSection,
},
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.sounds,
icon: 'fas fa-music',
bg: 'var(--bg)',
},
sounds: {},
}
},
computed: {
masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
get() { return ColdDeviceStorage.get('sound_masterVolume'); },
set(value) { ColdDeviceStorage.set('sound_masterVolume', value); }
async function edit(type) {
const { canceled, result } = await os.form(i18n.t('_sfx.' + type), {
type: {
type: 'enum',
enum: soundsTypes.map(x => ({
value: x,
label: x == null ? i18n.ts.none : x,
})),
label: i18n.ts.sound,
default: sounds.value[type].type,
},
volumeIcon() {
return this.masterVolume === 0 ? 'fas fa-volume-mute' : 'fas fa-volume-up';
}
},
created() {
this.sounds.note = ColdDeviceStorage.get('sound_note');
this.sounds.noteMy = ColdDeviceStorage.get('sound_noteMy');
this.sounds.notification = ColdDeviceStorage.get('sound_notification');
this.sounds.chat = ColdDeviceStorage.get('sound_chat');
this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
this.sounds.channel = ColdDeviceStorage.get('sound_channel');
},
methods: {
async edit(type) {
const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
type: {
type: 'enum',
enum: soundsTypes.map(x => ({
value: x,
label: x == null ? this.$ts.none : x,
})),
label: this.$ts.sound,
default: this.sounds[type].type,
},
volume: {
type: 'range',
mim: 0,
max: 1,
step: 0.05,
textConverter: (v) => `${Math.floor(v * 100)}%`,
label: this.$ts.volume,
default: this.sounds[type].volume
},
listen: {
type: 'button',
content: this.$ts.listen,
action: (_, values) => {
playFile(values.type, values.volume);
}
}
});
if (canceled) return;
const v = {
type: result.type,
volume: result.volume,
};
ColdDeviceStorage.set('sound_' + type, v);
this.sounds[type] = v;
volume: {
type: 'range',
mim: 0,
max: 1,
step: 0.05,
textConverter: (v) => `${Math.floor(v * 100)}%`,
label: i18n.ts.volume,
default: sounds.value[type].volume
},
reset() {
for (const sound of Object.keys(this.sounds)) {
const v = ColdDeviceStorage.default['sound_' + sound];
ColdDeviceStorage.set('sound_' + sound, v);
this.sounds[sound] = v;
listen: {
type: 'button',
content: i18n.ts.listen,
action: (_, values) => {
playFile(values.type, values.volume);
}
}
});
if (canceled) return;
const v = {
type: result.type,
volume: result.volume,
};
ColdDeviceStorage.set('sound_' + type, v);
sounds.value[type] = v;
}
function reset() {
for (const sound of Object.keys(sounds.value)) {
const v = ColdDeviceStorage.default['sound_' + sound];
ColdDeviceStorage.set('sound_' + sound, v);
sounds.value[sound] = v;
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.sounds,
icon: 'fas fa-music',
bg: 'var(--bg)',
}
});
</script>

View File

@ -13,7 +13,7 @@
<script lang="ts" setup>
import { } from 'vue';
import * as JSON5 from 'json5';
import JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormButton from '@/components/ui/button.vue';
import { applyTheme, validateTheme } from '@/scripts/theme';
@ -29,7 +29,7 @@ function parseThemeCode(code: string) {
try {
theme = JSON5.parse(code);
} catch (e) {
} catch (err) {
os.alert({
type: 'error',
text: i18n.ts._theme.invalid

View File

@ -1,95 +1,77 @@
<template>
<div class="_formRoot">
<FormSelect v-model="selectedThemeId" class="_formBlock">
<template #label>{{ $ts.theme }}</template>
<optgroup :label="$ts._theme.installedThemes">
<template #label>{{ i18n.ts.theme }}</template>
<optgroup :label="i18n.ts._theme.installedThemes">
<option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$ts._theme.builtinThemes">
<optgroup :label="i18n.ts._theme.builtinThemes">
<option v-for="x in builtinThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<template v-if="selectedTheme">
<FormInput readonly :modelValue="selectedTheme.author" class="_formBlock">
<template #label>{{ $ts.author }}</template>
<FormInput readonly :model-value="selectedTheme.author" class="_formBlock">
<template #label>{{ i18n.ts.author }}</template>
</FormInput>
<FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc" class="_formBlock">
<template #label>{{ $ts._theme.description }}</template>
<FormTextarea v-if="selectedTheme.desc" readonly :model-value="selectedTheme.desc" class="_formBlock">
<template #label>{{ i18n.ts._theme.description }}</template>
</FormTextarea>
<FormTextarea readonly tall :modelValue="selectedThemeCode" class="_formBlock">
<template #label>{{ $ts._theme.code }}</template>
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template>
<FormTextarea readonly tall :model-value="selectedThemeCode" class="_formBlock">
<template #label>{{ i18n.ts._theme.code }}</template>
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template>
</FormTextarea>
<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.uninstall }}</FormButton>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
<script lang="ts" setup>
import { computed, defineExpose, ref } from 'vue';
import JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import { Theme, builtinThemes } from '@/scripts/theme';
import { Theme, getBuiltinThemesRef } from '@/scripts/theme';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import { getThemes, removeTheme } from '@/theme-store';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormInput,
FormButton,
},
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
const selectedThemeId = ref(null);
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts._theme.manage,
icon: 'fas fa-folder-open',
bg: 'var(--bg)',
},
installedThemes: getThemes(),
builtinThemes,
selectedThemeId: null,
}
},
const themes = computed(() => [ ...installedThemes.value, ...builtinThemes.value ]);
computed: {
themes(): Theme[] {
return this.builtinThemes.concat(this.installedThemes);
},
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
const selectedTheme = computed(() => {
if (selectedThemeId.value == null) return null;
return themes.value.find(x => x.id === selectedThemeId.value);
});
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
},
const selectedThemeCode = computed(() => {
if (selectedTheme.value == null) return null;
return JSON5.stringify(selectedTheme.value, null, '\t');
});
methods: {
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
os.success();
},
function copyThemeCode() {
copyToClipboard(selectedThemeCode.value);
os.success();
}
uninstall() {
removeTheme(this.selectedTheme);
this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId);
this.selectedThemeId = null;
os.success();
},
function uninstall() {
removeTheme(selectedTheme.value as Theme);
installedThemes.value = installedThemes.value.filter(t => t.id !== selectedThemeId.value);
selectedThemeId.value = null;
os.success();
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts._theme.manage,
icon: 'fas fa-folder-open',
bg: 'var(--bg)',
}
});
</script>

View File

@ -85,116 +85,94 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
import * as JSON5 from 'json5';
<script lang="ts" setup>
import { computed, onActivated, ref, watch } from 'vue';
import JSON5 from 'json5';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormGroup from '@/components/form/group.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/ui/button.vue';
import { builtinThemes } from '@/scripts/theme';
import { getBuiltinThemesRef } from '@/scripts/theme';
import { selectFile } from '@/scripts/select-file';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { ColdDeviceStorage } from '@/store';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { instance } from '@/instance';
import { concat, uniqueBy } from '@/scripts/array';
import { uniqueBy } from '@/scripts/array';
import { fetchThemes, getThemes } from '@/theme-store';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
FormSwitch,
FormSelect,
FormGroup,
FormSection,
FormLink,
FormButton,
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
const instanceThemes = [];
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
const themes = computed(() => uniqueBy([ ...instanceThemes, ...builtinThemes.value, ...installedThemes.value ], theme => theme.id));
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
const darkTheme = ColdDeviceStorage.ref('darkTheme');
const darkThemeId = computed({
get() {
return darkTheme.value.id;
},
set(id) {
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
}
});
const lightTheme = ColdDeviceStorage.ref('lightTheme');
const lightThemeId = computed({
get() {
return lightTheme.value.id;
},
set(id) {
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
}
});
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(localStorage.getItem('wallpaper'));
const themesCount = installedThemes.value.length;
emits: ['info'],
watch(syncDeviceDarkMode, () => {
if (syncDeviceDarkMode.value) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
});
setup(props, { emit }) {
const INFO = {
title: i18n.ts.theme,
icon: 'fas fa-palette',
bg: 'var(--bg)',
};
watch(wallpaper, () => {
if (wallpaper.value == null) {
localStorage.removeItem('wallpaper');
} else {
localStorage.setItem('wallpaper', wallpaper.value);
}
location.reload();
});
const installedThemes = ref(getThemes());
const instanceThemes = [];
if (instance.defaultLightTheme != null) instanceThemes.push(JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) instanceThemes.push(JSON5.parse(instance.defaultDarkTheme));
const themes = computed(() => uniqueBy(instanceThemes.concat(builtinThemes.concat(installedThemes.value)), theme => theme.id));
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
const darkTheme = ColdDeviceStorage.ref('darkTheme');
const darkThemeId = computed({
get() {
return darkTheme.value.id;
},
set(id) {
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id))
}
});
const lightTheme = ColdDeviceStorage.ref('lightTheme');
const lightThemeId = computed({
get() {
return lightTheme.value.id;
},
set(id) {
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id))
}
});
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(localStorage.getItem('wallpaper'));
const themesCount = installedThemes.value.length;
onActivated(() => {
fetchThemes().then(() => {
installedThemes.value = getThemes();
});
});
watch(syncDeviceDarkMode, () => {
if (syncDeviceDarkMode.value) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
});
fetchThemes().then(() => {
installedThemes.value = getThemes();
});
watch(wallpaper, () => {
if (wallpaper.value == null) {
localStorage.removeItem('wallpaper');
} else {
localStorage.setItem('wallpaper', wallpaper.value);
}
location.reload();
});
function setWallpaper(event) {
selectFile(event.currentTarget ?? event.target, null).then(file => {
wallpaper.value = file.url;
});
}
onActivated(() => {
fetchThemes().then(() => {
installedThemes.value = getThemes();
});
});
fetchThemes().then(() => {
installedThemes.value = getThemes();
});
return {
[symbols.PAGE_INFO]: INFO,
darkThemes,
lightThemes,
darkThemeId,
lightThemeId,
darkMode,
syncDeviceDarkMode,
themesCount,
wallpaper,
setWallpaper(e) {
selectFile(e.currentTarget ?? e.target, null).then(file => {
wallpaper.value = file.url;
});
},
};
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.theme,
icon: 'fas fa-palette',
bg: 'var(--bg)',
}
});
</script>

View File

@ -43,6 +43,14 @@ import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
defineExpose({
[symbols.PAGE_INFO]: {
title: 'Edit webhook',
icon: 'fas fa-bolt',
bg: 'var(--bg)',
},
});
const webhook = await os.api('i/webhooks/show', {
webhookId: new URLSearchParams(window.location.search).get('id')
});
@ -78,12 +86,4 @@ async function save(): Promise<void> {
active,
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: 'Edit webhook',
icon: 'fas fa-bolt',
bg: 'var(--bg)',
},
});
</script>

View File

@ -1,35 +1,35 @@
<template>
<div class="_formRoot">
<MkTab v-model="tab" class="_formBlock">
<option value="soft">{{ $ts._wordMute.soft }}</option>
<option value="hard">{{ $ts._wordMute.hard }}</option>
<option value="soft">{{ i18n.ts._wordMute.soft }}</option>
<option value="hard">{{ i18n.ts._wordMute.hard }}</option>
</MkTab>
<div class="_formBlock">
<div v-show="tab === 'soft'">
<MkInfo class="_formBlock">{{ $ts._wordMute.softDescription }}</MkInfo>
<MkInfo class="_formBlock">{{ i18n.ts._wordMute.softDescription }}</MkInfo>
<FormTextarea v-model="softMutedWords" class="_formBlock">
<span>{{ $ts._wordMute.muteWords }}</span>
<template #caption>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
<span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
</FormTextarea>
</div>
<div v-show="tab === 'hard'">
<MkInfo class="_formBlock">{{ $ts._wordMute.hardDescription }} {{ $ts.reflectMayTakeTime }}</MkInfo>
<MkInfo class="_formBlock">{{ i18n.ts._wordMute.hardDescription }} {{ i18n.ts.reflectMayTakeTime }}</MkInfo>
<FormTextarea v-model="hardMutedWords" class="_formBlock">
<span>{{ $ts._wordMute.muteWords }}</span>
<template #caption>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
<span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template>
</FormTextarea>
<MkKeyValue v-if="hardWordMutedNotesCount != null" class="_formBlock">
<template #key>{{ $ts._wordMute.mutedNotes }}</template>
<template #key>{{ i18n.ts._wordMute.mutedNotes }}</template>
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
</MkKeyValue>
</div>
</div>
<MkButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton primary inline :disabled="!changed" @click="save()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineExpose, ref, watch } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import MkKeyValue from '@/components/key-value.vue';
import MkButton from '@/components/ui/button.vue';
@ -38,114 +38,90 @@ import MkTab from '@/components/tab.vue';
import * as os from '@/os';
import number from '@/filters/number';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
FormTextarea,
MkKeyValue,
MkTab,
MkInfo,
},
const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) {
return x.join(' ');
} else {
return x;
}
}).join('\n');
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.wordMute,
icon: 'fas fa-comment-slash',
bg: 'var(--bg)',
},
tab: 'soft',
softMutedWords: '',
hardMutedWords: '',
hardWordMutedNotesCount: null,
changed: false,
}
},
const tab = ref('soft');
const softMutedWords = ref(render(defaultStore.state.mutedWords));
const hardMutedWords = ref(render($i!.mutedWords));
const hardWordMutedNotesCount = ref(null);
const changed = ref(false);
watch: {
softMutedWords: {
handler() {
this.changed = true;
},
deep: true
},
hardMutedWords: {
handler() {
this.changed = true;
},
deep: true
},
},
os.api('i/get-word-muted-notes-count', {}).then(response => {
hardWordMutedNotesCount.value = response?.count;
});
async created() {
const render = (mutedWords) => mutedWords.map(x => {
if (Array.isArray(x)) {
return x.join(' ');
} else {
return x;
}
}).join('\n');
watch(softMutedWords, () => {
changed.value = true;
});
this.softMutedWords = render(this.$store.state.mutedWords);
this.hardMutedWords = render(this.$i.mutedWords);
watch(hardMutedWords, () => {
changed.value = true;
});
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
},
async function save() {
const parseMutes = (mutes, tab) => {
// split into lines, remove empty lines and unnecessary whitespace
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line !== '');
methods: {
async save() {
const parseMutes = (mutes, tab) => {
// split into lines, remove empty lines and unnecessary whitespace
let lines = mutes.trim().split('\n').map(line => line.trim()).filter(line => line != '');
// check each line if it is a RegExp or not
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const regexp = line.match(/^\/(.+)\/(.*)$/);
if (regexp) {
// check that the RegExp is valid
try {
new RegExp(regexp[1], regexp[2]);
// note that regex lines will not be split by spaces!
} catch (err) {
// invalid syntax: do not save, do not reset changed flag
os.alert({
type: 'error',
title: this.$ts.regexpError,
text: this.$t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString()
});
// re-throw error so these invalid settings are not saved
throw err;
}
} else {
lines[i] = line.split(' ');
}
// check each line if it is a RegExp or not
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const regexp = line.match(/^\/(.+)\/(.*)$/);
if (regexp) {
// check that the RegExp is valid
try {
new RegExp(regexp[1], regexp[2]);
// note that regex lines will not be split by spaces!
} catch (err: any) {
// invalid syntax: do not save, do not reset changed flag
os.alert({
type: 'error',
title: i18n.ts.regexpError,
text: i18n.t('regexpErrorDescription', { tab, line: i + 1 }) + "\n" + err.toString()
});
// re-throw error so these invalid settings are not saved
throw err;
}
return lines;
};
let softMutes, hardMutes;
try {
softMutes = parseMutes(this.softMutedWords, this.$ts._wordMute.soft);
hardMutes = parseMutes(this.hardMutedWords, this.$ts._wordMute.hard);
} catch (err) {
// already displayed error message in parseMutes
return;
} else {
lines[i] = line.split(' ');
}
}
this.$store.set('mutedWords', softMutes);
await os.api('i/update', {
mutedWords: hardMutes,
});
return lines;
};
this.changed = false;
},
let softMutes, hardMutes;
try {
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
} catch (err) {
// already displayed error message in parseMutes
return;
}
number
defaultStore.set('mutedWords', softMutes);
await os.api('i/update', {
mutedWords: hardMutes,
});
changed.value = false;
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.wordMute,
icon: 'fas fa-comment-slash',
bg: 'var(--bg)',
}
});
</script>

View File

@ -56,7 +56,7 @@ export default defineComponent({
localOnly: null as boolean | null,
files: [] as Misskey.entities.DriveFile[],
visibleUsers: [] as Misskey.entities.User[],
}
};
},
async created() {
@ -153,11 +153,11 @@ export default defineComponent({
);
}
//#endregion
} catch (e) {
} catch (err) {
os.alert({
type: 'error',
title: e.message,
text: e.name
title: err.message,
text: err.name
});
}

View File

@ -67,15 +67,17 @@
<script lang="ts" setup>
import { watch } from 'vue';
import { toUnicode } from 'punycode/';
import * as tinycolor from 'tinycolor2';
import { v4 as uuid} from 'uuid';
import * as JSON5 from 'json5';
import tinycolor from 'tinycolor2';
import { v4 as uuid } from 'uuid';
import JSON5 from 'json5';
import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormFolder from '@/components/form/folder.vue';
import { Theme, applyTheme, darkTheme, lightTheme } from '@/scripts/theme';
import { Theme, applyTheme } from '@/scripts/theme';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
import { host } from '@/config';
import * as os from '@/os';
import { ColdDeviceStorage, defaultStore } from '@/store';
@ -124,11 +126,11 @@ let changed = $ref(false);
useLeaveGuard($$(changed));
function showPreview() {
os.pageWindow('preview');
os.pageWindow('/preview');
}
function setBgColor(color: typeof bgColors[number]) {
if (theme.base != color.kind) {
if (theme.base !== color.kind) {
const base = color.kind === 'dark' ? darkTheme : lightTheme;
for (const prop of Object.keys(base.props)) {
if (prop === 'accent') continue;

View File

@ -20,7 +20,7 @@
<script lang="ts">
export default {
name: 'MkTimelinePage',
}
};
</script>
<script lang="ts" setup>

View File

@ -54,6 +54,9 @@
<FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
</FormSection>
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<MkObjectView tall :value="user">
</MkObjectView>
</div>
@ -232,10 +235,10 @@ export default defineComponent({
await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
os.success();
};
await process().catch(e => {
await process().catch(err => {
os.alert({
type: 'error',
text: e.toString()
text: err.toString(),
});
});
await this.refreshUser();

View File

@ -125,7 +125,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent, computed } from 'vue';
import * as age from 's-age';
import age from 's-age';
import XUserTimeline from './index.timeline.vue';
import XNote from '@/components/note.vue';
import MkFollowButton from '@/components/follow-button.vue';
@ -141,6 +141,7 @@ import number from '@/filters/number';
import { userPage, acct as getAcct } from '@/filters/user';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { MisskeyNavigator } from '@/scripts/navigate';
export default defineComponent({
components: {
@ -190,33 +191,34 @@ export default defineComponent({
active: this.page === 'index',
title: this.$ts.overview,
icon: 'fas fa-home',
onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
onClick: () => { this.mkNav.push('/@' + getAcct(this.user)); },
}, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{
active: this.page === 'reactions',
title: this.$ts.reaction,
icon: 'fas fa-laugh',
onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); },
onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/reactions'); },
}] : [], {
active: this.page === 'clips',
title: this.$ts.clips,
icon: 'fas fa-paperclip',
onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/clips'); },
onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/clips'); },
}, {
active: this.page === 'pages',
title: this.$ts.pages,
icon: 'fas fa-file-alt',
onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/pages'); },
onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/pages'); },
}, {
active: this.page === 'gallery',
title: this.$ts.gallery,
icon: 'fas fa-icons',
onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/gallery'); },
onClick: () => { this.mkNav.push('/@' + getAcct(this.user) + '/gallery'); },
}],
} : null),
user: null,
error: null,
parallaxAnimationId: null,
narrow: null,
mkNav: new MisskeyNavigator(),
};
},
@ -258,8 +260,8 @@ export default defineComponent({
this.user = null;
os.api('users/show', Acct.parse(this.acct)).then(user => {
this.user = user;
}).catch(e => {
this.error = e;
}).catch(err => {
this.error = err;
});
},

View File

@ -41,7 +41,7 @@ export default defineComponent({
password: '',
submitting: false,
host,
}
};
},
methods: {

View File

@ -39,7 +39,7 @@ export default defineComponent({
return {
notes: [],
isScrolling: false,
}
};
},
created() {