Migrate to Vue3 (#6587)

* Update reaction.vue

* fix  bug

* wip

* wip

* wjio

* wip

* Revert "wip"

This reverts commit e427f2160a.

* wip

* wip

* wip

* Update init.ts

* Update drive-window.vue

* wip

* wip

* Use PascalCase for components

* Use PascalCase for components

* update dep

* wip

* wip

* wip

* Update init.ts

* wip

* Update paging.ts

* Update test.vue

* watch deep

* wip

* lint

* wip

* wip

* wip

* wip

* wiop

* wip

* Update webpack.config.ts

* alllow null poll

* wip

* wip

* wip

* wiop

* UI redesign & refactor (#6714)

* wip

* wip

* wip

* wip

* wip

* Update drive.vue

* Update word-mute.vue

* wip

* wip

* wip

* clean up

* wip

* Update default.vue

* wip

* Update notes.vue

* Update mfm.ts

* Update index.home.vue

* Update post-form.vue

* Update post-form-attaches.vue

* wip

* Update post-form.vue

* Update sidebar.vue

* wip

* wip

* Update index.vue

* wip

* Update default.vue

* Update index.vue

* Update index.vue

* wip

* Update post-form-attaches.vue

* Update note.vue

* wip

* clean up

* Update notes.vue

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* Update index.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update default.vue

* wip

* Update _dark.json5

* wip

* wip

* wip

* clean up

* wip

* wip

* Update index.vue

* Update test.vue

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* clena yop

* wip

* wip

* Update store.ts

* Update messaging-room.vue

* Update default.widgets.vue

* fix

* wip

* wip

* Update modal.vue

* wip

* Update os.ts

* Update os.ts

* Update deck.vue

* Update init.ts

* wip

* Update ja-JP.yml

* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない

* Update modal.vue

* wip

* Update tooltip.ts

* wip

* wip

* wip

* wip

* wip

* Update image-viewer.vue

* wip

* wip

* Update style.scss

* Update style.scss

* Update visitor.vue

* wip

* Update init.ts

* Update init.ts

* wip

* wip

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* wip

* wip

* Update modal.vue

* Update header.vue

* Update menu.vue

* Update about.vue

* Update about-misskey.vue

* wip

* wip

* Update visitor.vue

* Update tooltip.ts

* wip

* Update drive.vue

* wip

* Update style.scss

* Update header.vue

* wip

* wip

* Update users.user.vue

* Update announcements.vue

* wip

* wip

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update style.scss

* Update users.vue

* wip

* Update style.scss

* wip

* Update welcome.entrance.vue

* Update radio.vue

* Update size.ts

* Update emoji-edit-dialog.vue

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* wip

* wip

* wip

* wip

* Update file-dialog.vue

* wip

* wip

* Update token-generate-window.vue

* Update notification-setting-window.vue

* wip

* wip

* Update _error_.vue

* Update ja-JP.yml

* wip

* wip

* Update store.ts

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* Update announcements.vue

* Update store.ts

* wip

* Update page-editor.vue

* wip

* wip

* Update modal.vue

* wip

* Update select-file.ts

* Update timeline.vue

* Update emojis.vue

* Update os.ts

* wip

* Update user-select.vue

* Update mfm.ts

* Update get-file-info.ts

* Update drive.vue

* Update init.ts

* Update mfm.ts

* wip

* wip

* Update window.vue

* Update note.vue

* wip

* wip

* Update user-info.vue

* wip

* wip

* wip

* wip

* wip

* Update header.vue

* Update header.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update webpack.config.ts

* wip

* wip

* wip

* wip

* wip

* wip

* Update autocomplete.ts

* wip

* wip

* wip

* Update toast.vue

* wip

* Update post-form-dialog.vue

* wip

* wip

* wip

* wip

* wip

* Update users.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update package.json

* wip

* Update icon-dialog.vue

* wip

* wip

* Update user-preview.ts

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* Update user-name.vue

* Update federation.vue

* Update instance.vue

* wip

* wip

* Update tag.vue

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* wip

* Update os.ts

* Update os.ts

* wip

* wip

* wip

* Update router.ts

* wip

* Update init.ts

* Update note.vue

* Update messages.vue

* wip

* wip

* wip

* wip

* wip

* google

* wip

* wip

* wip

* wip

* Update theme-editor.vue

* wip

* wip

* Update room.vue

* Update channel-editor.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update menu.vue

* wip

* wip

* wip

* wip

* Update messaging-room.vue

* wip

* Update post-form.vue

* Update default.widgets.vue

* Update window.vue

* wip
This commit is contained in:
syuilo
2020-10-17 20:12:00 +09:00
committed by GitHub
parent a40f38b2b5
commit 7199e6f4e0
357 changed files with 15053 additions and 12496 deletions

View File

@@ -0,0 +1,59 @@
<template>
<section class="_section">
<div class="_content">
<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faKey } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton, MkInput
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: 'API',
icon: faKey
}]
},
};
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
async generateToken() {
os.popup(await 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.dialog({
type: 'success',
title: this.$t('token'),
text: token
});
},
}, 'closed');
},
}
});
</script>

View File

@@ -0,0 +1,60 @@
<template>
<section class="uawsfosz _section">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('drive') }}</div>
<div class="_content">
<span>{{ $t('uploadFolder') }}: {{ uploadFolder ? uploadFolder.name : '-' }}</span>
<MkButton primary @click="chooseUploadFolder()"><Fa :icon="faFolderOpen"/> {{ $t('selectFolder') }}</MkButton>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCloud, faFolderOpen } from '@fortawesome/free-solid-svg-icons';
import { faClock, faEyeSlash, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
},
data() {
return {
uploadFolder: null,
faCloud, faClock, faEyeSlash, faFolderOpen, faTrashAlt
}
},
async created() {
if (this.$store.state.settings.uploadFolder) {
this.uploadFolder = await os.api('drive/folders/show', {
folderId: this.$store.state.settings.uploadFolder
});
}
},
methods: {
chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
await this.$store.dispatch('settings/set', { key: 'uploadFolder', value: folder ? folder.id : null });
os.success();
if (this.$store.state.settings.uploadFolder) {
this.uploadFolder = await os.api('drive/folders/show', {
folderId: this.$store.state.settings.uploadFolder
});
} else {
this.uploadFolder = null;
}
});
}
}
});
</script>
<style lang="scss" scoped>
.uawsfosz {
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<div class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<div>{{ $t('whenServerDisconnected') }}</div>
<MkRadio v-model="serverDisconnectedBehavior" value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</MkRadio>
<MkRadio v-model="serverDisconnectedBehavior" value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</MkRadio>
<MkRadio v-model="serverDisconnectedBehavior" value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</MkRadio>
</div>
<div class="_content">
<MkSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</MkSwitch>
<MkSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</MkSwitch>
<MkSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</MkSwitch>
<MkSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('chatOpenBehavior') }}</div>
<MkRadio v-model="chatOpenBehavior" value="page">{{ $t('showInPage') }}</MkRadio>
<MkRadio v-model="chatOpenBehavior" value="window">{{ $t('openInWindow') }}</MkRadio>
<MkRadio v-model="chatOpenBehavior" value="popout">{{ $t('popout') }}</MkRadio>
</div>
<div class="_content">
<MkSelect v-model:value="lang">
<template #label>{{ $t('uiLanguage') }}</template>
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</MkSelect>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div>
<div class="_content">
<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch>
<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch>
<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch>
<MkSwitch v-model:value="useOsNativeEmojis">
{{ $t('useOsNativeEmojis') }}
<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('fontSize') }}</div>
<MkRadio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></MkRadio>
<MkRadio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></MkRadio>
<MkRadio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></MkRadio>
<MkRadio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></MkRadio>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div>
<div class="_content">
<MkSwitch v-model:value="deckAlwaysShowMainColumn">
{{ $t('_deck.alwaysShowMainColumn') }}
</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('_deck.columnAlign') }}</div>
<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
</div>
</section>
<MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkSelect from '@/components/ui/select.vue';
import MkRadio from '@/components/ui/radio.vue';
import MkRange from '@/components/ui/range.vue';
import { langs } from '@/config';
import { clientDb, set } from '@/db';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
MkSelect,
MkRadio,
MkRange,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('general'),
icon: faCogs
}]
},
langs,
lang: localStorage.getItem('lang'),
fontSize: localStorage.getItem('fontSize'),
faImage, faCog, faColumns
}
},
computed: {
serverDisconnectedBehavior: {
get() { return this.$store.state.device.serverDisconnectedBehavior; },
set(value) { this.$store.commit('device/set', { key: 'serverDisconnectedBehavior', value }); }
},
reduceAnimation: {
get() { return !this.$store.state.device.animation; },
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
},
useBlurEffectForModal: {
get() { return this.$store.state.device.useBlurEffectForModal; },
set(value) { this.$store.commit('device/set', { key: 'useBlurEffectForModal', value: value }); }
},
disableAnimatedMfm: {
get() { return !this.$store.state.device.animatedMfm; },
set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
},
useOsNativeEmojis: {
get() { return this.$store.state.device.useOsNativeEmojis; },
set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
},
imageNewTab: {
get() { return this.$store.state.device.imageNewTab; },
set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
},
disablePagesScript: {
get() { return this.$store.state.device.disablePagesScript; },
set(value) { this.$store.commit('device/set', { key: 'disablePagesScript', value }); }
},
showFixedPostForm: {
get() { return this.$store.state.device.showFixedPostForm; },
set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); }
},
chatOpenBehavior: {
get() { return this.$store.state.device.chatOpenBehavior; },
set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
},
enableInfiniteScroll: {
get() { return this.$store.state.device.enableInfiniteScroll; },
set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
},
deckAlwaysShowMainColumn: {
get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
},
deckColumnAlign: {
get() { return this.$store.state.device.deckColumnAlign; },
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
},
},
watch: {
lang() {
localStorage.setItem('lang', this.lang);
return set('_version_', `changeLang-${(new Date()).toJSON()}`, clientDb.i18n)
.then(() => location.reload())
.catch(() => {
os.dialog({
type: 'error',
});
});
},
fontSize() {
if (this.fontSize == null) {
localStorage.removeItem('fontSize');
} else {
localStorage.setItem('fontSize', this.fontSize);
}
location.reload();
},
enableInfiniteScroll() {
location.reload()
},
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
cacheClear() {
// Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
navigator.serviceWorker.getRegistrations().then(registrations => {
for (const registration of registrations) registration.unregister();
});
} catch (e) {
console.error(e);
}
// Force reload
location.reload(true);
}
}
});
</script>

View File

@@ -0,0 +1,119 @@
<template>
<section class="_section">
<div class="_title"><Fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div>
<div class="_content">
<MkSelect v-model:value="exportTarget">
<option value="notes">{{ $t('_exportOrImport.allNotes') }}</option>
<option value="following">{{ $t('_exportOrImport.followingList') }}</option>
<option value="user-lists">{{ $t('_exportOrImport.userLists') }}</option>
<option value="mute">{{ $t('_exportOrImport.muteList') }}</option>
<option value="blocking">{{ $t('_exportOrImport.blockingList') }}</option>
</MkSelect>
<MkButton inline @click="doExport()"><Fa :icon="faDownload"/> {{ $t('export') }}</MkButton>
<MkButton inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><Fa :icon="faUpload"/> {{ $t('import') }}</MkButton>
</div>
<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import { apiUrl } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSelect,
},
data() {
return {
exportTarget: 'notes',
faDownload, faUpload, faBoxes
}
},
methods: {
doExport() {
os.api(
this.exportTarget == 'notes' ? 'i/export-notes' :
this.exportTarget == 'following' ? 'i/export-following' :
this.exportTarget == 'blocking' ? 'i/export-blocking' :
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
null, {})
.then(() => {
os.dialog({
type: 'info',
text: this.$t('exportRequested')
});
}).catch((e: any) => {
os.dialog({
type: 'error',
text: e.message
});
});
},
doImport() {
(this.$refs.file as any).click();
},
onChangeFile() {
const [file] = Array.from((this.$refs.file as any).files);
const data = new FormData();
data.append('file', file);
data.append('i', this.$store.state.i.token);
const dialog = os.dialog({
type: 'waiting',
text: this.$t('uploading') + '...',
showOkButton: false,
showCancelButton: false,
cancelableByBgClick: false
});
fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: data
})
.then(response => response.json())
.then(f => {
this.reqImport(f);
})
.catch(e => {
os.dialog({
type: 'error',
text: e
});
})
.finally(() => {
dialog.close();
});
},
reqImport(file) {
os.api(
this.exportTarget == 'following' ? 'i/import-following' :
this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
null, {
fileId: file.id
}).then(() => {
os.dialog({
type: 'info',
text: this.$t('importRequested')
});
}).catch((e: any) => {
os.dialog({
type: 'error',
text: e.message
});
});
}
}
});
</script>

View File

@@ -0,0 +1,154 @@
<template>
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
<div class="nav" v-if="!narrow || $route.name === 'settings'">
<div class="menu">
<div class="label">{{ $t('basicSettings') }}</div>
<router-link class="item" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</router-link>
<router-link class="item" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</router-link>
<router-link class="item" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</router-link>
<router-link class="item" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</router-link>
<router-link class="item" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</router-link>
<router-link class="item" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</router-link>
</div>
<div class="menu">
<div class="label">{{ $t('clientSettings') }}</div>
<router-link class="item" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</router-link>
<router-link class="item" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</router-link>
<router-link class="item" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</router-link>
<router-link class="item" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</router-link>
<router-link class="item" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</router-link>
</div>
<div class="menu">
<div class="label">{{ $t('otherSettings') }}</div>
<router-link class="item" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</router-link>
<router-link class="item" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</router-link>
<router-link class="item" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</router-link>
<router-link class="item" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</router-link>
</div>
<div class="menu">
<button class="_button item" @click="logout">{{ $t('logout') }}</button>
</div>
</div>
<div class="main">
<router-view v-slot="{ Component }">
<transition :name="($store.state.device.animation && !narrow) ? 'view-slide' : ''" appear mode="out-in">
<component :is="Component" @info="onInfo"/>
</transition>
</router-view>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey } from '@fortawesome/free-solid-svg-icons';
import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons';
import { store } from '@/store';
import { i18n } from '@/i18n';
export default defineComponent({
setup(props, context) {
const INFO = ref({
header: [{
title: i18n.global.t('settings'),
icon: faCog
}]
});
const narrow = ref(false);
const view = ref(null);
const el = ref(null);
const onInfo = (viewInfo) => {
INFO.value = viewInfo;
};
onMounted(() => {
narrow.value = el.value.offsetWidth < 650;
});
return {
INFO,
narrow,
view,
el,
onInfo,
logout: () => {
store.dispatch('logout');
location.href = '/';
},
faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey,
};
},
});
</script>
<style lang="scss" scoped>
.view-slide-enter-active, .view-slide-leave-active {
transition: opacity 0.3s, transform 0.3s !important;
}
.view-slide-enter-from, .view-slide-leave-to {
opacity: 0;
transform: translateX(32px);
}
.vvcocwet {
max-width: 1000px;
margin: 0 auto;
> .nav {
> .menu {
margin: 16px 0;
> .label {
padding: 8px 32px;
font-size: 80%;
opacity: 0.7;
}
> .item {
display: block;
width: 100%;
box-sizing: border-box;
padding: 0 32px;
line-height: 48px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
//background: var(--panel);
//border-bottom: solid 1px var(--divider);
transition: padding 0.2s ease, color 0.1s ease;
&:first-of-type {
//border-top: solid 1px var(--divider);
}
&.router-link-active {
color: var(--accent);
padding-left: 42px;
}
&:hover {
text-decoration: none;
padding-left: 42px;
}
> .icon {
margin-right: 0.5em;
}
}
}
}
&.wide {
display: flex;
> .nav {
width: 30%;
max-width: 260px;
}
> .main {
flex: 1;
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<section class="_section" v-if="enableTwitterIntegration || enableDiscordIntegration || enableGithubIntegration">
<div class="_content" v-if="enableTwitterIntegration">
<header><Fa :icon="faTwitter"/> Twitter</header>
<p v-if="integrations.twitter">{{ $t('connectedTo') }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter">{{ $t('disconnectSerice') }}</MkButton>
<MkButton v-else @click="connectTwitter">{{ $t('connectSerice') }}</MkButton>
</div>
<div class="_content" v-if="enableDiscordIntegration">
<header><Fa :icon="faDiscord"/> Discord</header>
<p v-if="integrations.discord">{{ $t('connectedTo') }}: <a :href="`https://discordapp.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord">{{ $t('disconnectSerice') }}</MkButton>
<MkButton v-else @click="connectDiscord">{{ $t('connectSerice') }}</MkButton>
</div>
<div class="_content" v-if="enableGithubIntegration">
<header><Fa :icon="faGithub"/> GitHub</header>
<p v-if="integrations.github">{{ $t('connectedTo') }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub">{{ $t('disconnectSerice') }}</MkButton>
<MkButton v-else @click="connectGithub">{{ $t('connectSerice') }}</MkButton>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import { apiUrl } from '@/config';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('integration'),
icon: faShareAlt
}]
},
apiUrl,
twitterForm: null,
discordForm: null,
githubForm: null,
enableTwitterIntegration: false,
enableDiscordIntegration: false,
enableGithubIntegration: false,
faShareAlt, faTwitter, faDiscord, faGithub
};
},
computed: {
integrations() {
return this.$store.state.i.integrations;
},
meta() {
return this.$store.state.instance.meta;
},
},
created() {
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
},
mounted() {
this.$emit('info', this.INFO);
document.cookie = `igi=${this.$store.state.i.token}; path=/;` +
` max-age=31536000;` +
(document.location.protocol.startsWith('https') ? ' secure' : '');
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
});
},
methods: {
connectTwitter() {
this.twitterForm = window.open(apiUrl + '/connect/twitter',
'twitter_connect_window',
'height=570, width=520');
},
disconnectTwitter() {
window.open(apiUrl + '/disconnect/twitter',
'twitter_disconnect_window',
'height=570, width=520');
},
connectDiscord() {
this.discordForm = window.open(apiUrl + '/connect/discord',
'discord_connect_window',
'height=570, width=520');
},
disconnectDiscord() {
window.open(apiUrl + '/disconnect/discord',
'discord_disconnect_window',
'height=570, width=520');
},
connectGithub() {
this.githubForm = window.open(apiUrl + '/connect/github',
'github_connect_window',
'height=570, width=520');
},
disconnectGithub() {
window.open(apiUrl + '/disconnect/github',
'github_disconnect_window',
'height=570, width=520');
},
}
});
</script>

View File

@@ -0,0 +1,93 @@
<template>
<section class="rrfwjxfl _section">
<MkTab v-model:value="tab" :items="[{ label: $t('mutedUsers'), value: 'mute' }, { label: $t('blockedUsers'), value: 'block' }]" style="margin-bottom: var(--margin);"/>
<div class="_content" v-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination" class="muting">
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>
<template #default="{items}">
<div class="user" v-for="mute in items" :key="mute.id">
<router-link class="name" :to="userPage(mute.mutee)">
<MkAcct :user="mute.mutee"/>
</router-link>
</div>
</template>
</MkPagination>
</div>
<div class="_content" v-if="tab === 'block'">
<MkPagination :pagination="blockingPagination" class="blocking">
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>
<template #default="{items}">
<div class="user" v-for="block in items" :key="block.id">
<router-link class="name" :to="userPage(block.blockee)">
<MkAcct :user="block.blockee"/>
</router-link>
</div>
</template>
</MkPagination>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faBan } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue';
import MkInfo from '@/components/ui/info.vue';
import { userPage } from '@/filters/user';
import * as os from '@/os';
export default defineComponent({
components: {
MkPagination,
MkTab,
MkInfo,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('muteAndBlock'),
icon: faBan
}]
},
tab: 'mute',
mutingPagination: {
endpoint: 'mute/list',
limit: 10,
},
blockingPagination: {
endpoint: 'blocking/list',
limit: 10,
},
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
userPage
}
});
</script>
<style lang="scss" scoped>
.rrfwjxfl {
> ._content {
max-height: 350px;
overflow: auto;
> .muting,
> .blocking {
> .empty {
opacity: 0.5 !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div>
<div class="_section">
<MkButton full primary @click="configure"><Fa :icon="faCog"/> {{ $t('notificationSetting') }}</MkButton>
</div>
<div class="_section">
<div class="_card">
<div class="_content">
<MkSwitch v-model:value="$store.state.i.autoWatch" @update:value="onChangeAutoWatch">
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
</MkSwitch>
</div>
</div>
</div>
<div class="_section">
<MkButton full @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</MkButton>
<MkButton full @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</MkButton>
<MkButton full @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import { notificationTypes } from '../../../types';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('notifications'),
icon: faBell
}]
},
faCog
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
onChangeAutoWatch(v) {
os.api('i/update', {
autoWatch: v
});
},
readAllUnreadNotes() {
os.api('i/read-all-unread-notes');
},
readAllMessagingMessages() {
os.api('i/read-all-messaging-messages');
},
readAllNotifications() {
os.api('notifications/mark-all-as-read');
},
async configure() {
const includingTypes = notificationTypes.filter(x => !this.$store.state.i.mutingNotificationTypes.includes(x));
os.popup(await 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.$store.state.i.mutingNotificationTypes = i.mutingNotificationTypes;
});
}
}, 'closed');
},
}
});
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div class="_section">
<div class="_card">
<div class="_content">
<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
{{ $t('showFeaturedNotesInTimeline') }}
</MkSwitch>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkSelect,
MkSwitch,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('other'),
icon: faEllipsisH
}]
},
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
onChangeInjectFeaturedNote(v) {
os.api('i/update', {
injectFeaturedNote: v
});
},
}
});
</script>

View File

@@ -0,0 +1,200 @@
<template>
<section class="_section">
<div class="_title"><Fa :icon="faPlug"/> {{ $t('plugins') }}</div>
<div class="_content">
<details>
<summary><Fa :icon="faDownload"/> {{ $t('install') }}</summary>
<MkInfo warn>{{ $t('pluginInstallWarn') }}</MkInfo>
<MkTextarea v-model:value="script" tall>
<span>{{ $t('script') }}</span>
</MkTextarea>
<MkButton @click="install()" primary><Fa :icon="faSave"/> {{ $t('install') }}</MkButton>
</details>
</div>
<div class="_content">
<details>
<summary><Fa :icon="faFolderOpen"/> {{ $t('manage') }}</summary>
<MkSelect v-model:value="selectedPluginId">
<option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
</MkSelect>
<template v-if="selectedPlugin">
<div style="margin: -8px 0 8px 0;">
<MkSwitch :value="selectedPlugin.active" @update:value="changeActive(selectedPlugin, $event)">{{ $t('makeActive') }}</MkSwitch>
</div>
<div class="_keyValue">
<div>{{ $t('version') }}:</div>
<div>{{ selectedPlugin.version }}</div>
</div>
<div class="_keyValue">
<div>{{ $t('author') }}:</div>
<div>{{ selectedPlugin.author }}</div>
</div>
<div class="_keyValue">
<div>{{ $t('description') }}:</div>
<div>{{ selectedPlugin.description }}</div>
</div>
<div style="margin-top: 8px;">
<MkButton @click="config()" inline v-if="selectedPlugin.config"><Fa :icon="faCog"/> {{ $t('settings') }}</MkButton>
<MkButton @click="uninstall()" inline><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
</div>
</template>
</details>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInfo from '@/components/ui/info.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkSelect,
MkInfo,
MkSwitch,
},
data() {
return {
script: '',
selectedPluginId: null,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
}
},
computed: {
selectedPlugin() {
if (this.selectedPluginId == null) return null;
return this.$store.state.deviceUser.plugins.find(x => x.id === this.selectedPluginId);
},
},
methods: {
async install() {
let ast;
try {
ast = parse(this.script);
} catch (e) {
os.dialog({
type: 'error',
text: 'Syntax error :('
});
return;
}
const meta = AiScript.collectMetadata(ast);
if (meta == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const data = meta.get(null);
if (data == null) {
os.dialog({
type: 'error',
text: 'No metadata found :('
});
return;
}
const { name, version, author, description, permissions, config } = data;
if (name == null || version == null || author == null) {
os.dialog({
type: 'error',
text: 'Required property not found :('
});
return;
}
const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => {
os.popup(await import('@/components/token-generate-window.vue'), {
title: this.$t('tokenRequested'),
information: this.$t('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');
});
this.$store.commit('deviceUser/installPlugin', {
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
os.success();
this.$nextTick(() => {
location.reload();
});
},
uninstall() {
this.$store.commit('deviceUser/uninstallPlugin', this.selectedPluginId);
os.success();
this.$nextTick(() => {
location.reload();
});
},
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async config() {
const config = this.selectedPlugin.config;
for (const key in this.selectedPlugin.configData) {
config[key].default = this.selectedPlugin.configData[key];
}
const { canceled, result } = await os.form(this.selectedPlugin.name, config);
if (canceled) return;
this.$store.commit('deviceUser/configPlugin', {
id: this.selectedPluginId,
config: result
});
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
this.$store.commit('deviceUser/changePluginActive', {
id: plugin.id,
active: active
});
this.$nextTick(() => {
location.reload();
});
}
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div class="_section">
<div class="_card">
<div class="_content">
<MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch>
<MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch>
</div>
<div class="_content">
<MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch>
<MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
<template #label>{{ $t('defaultNoteVisibility') }}</template>
<option value="public">{{ $t('_visibility.public') }}</option>
<option value="home">{{ $t('_visibility.home') }}</option>
<option value="followers">{{ $t('_visibility.followers') }}</option>
<option value="specified">{{ $t('_visibility.specified') }}</option>
</MkSelect>
<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLockOpen } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkSelect,
MkSwitch,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('privacy'),
icon: faLockOpen
}]
},
isLocked: false,
autoAcceptFollowed: false,
}
},
computed: {
defaultNoteVisibility: {
get() { return this.$store.state.settings.defaultNoteVisibility; },
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
},
defaultNoteLocalOnly: {
get() { return this.$store.state.settings.defaultNoteLocalOnly; },
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
},
rememberNoteVisibility: {
get() { return this.$store.state.settings.rememberNoteVisibility; },
set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
},
},
created() {
this.isLocked = this.$store.state.i.isLocked;
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
save() {
os.api('i/update', {
isLocked: !!this.isLocked,
autoAcceptFollowed: !!this.autoAcceptFollowed,
});
}
}
});
</script>

View File

@@ -0,0 +1,232 @@
<template>
<div class="_section">
<div class="llvierxe _card">
<div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
<div class="_content">
<div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
</div>
<MkInput v-model:value="name" :max="30">
<span>{{ $t('_profile.name') }}</span>
</MkInput>
<MkTextarea v-model:value="description" :max="500">
<span>{{ $t('_profile.description') }}</span>
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
</MkTextarea>
<MkInput v-model:value="location">
<span>{{ $t('location') }}</span>
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
</MkInput>
<MkInput v-model:value="birthday" type="date">
<template #title>{{ $t('birthday') }}</template>
<template #prefix><Fa :icon="faBirthdayCake"/></template>
</MkInput>
<details class="fields">
<summary>{{ $t('_profile.metadata') }}</summary>
<div class="row">
<MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue0">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName1">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue1">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName2">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue2">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName3">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue3">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
</details>
<MkSwitch v-model:value="isBot">{{ $t('flagAsBot') }}</MkSwitch>
<MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch>
</div>
<div class="_footer">
<MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
import { faSave } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSwitch from '@/components/ui/switch.vue';
import { host } from '@/config';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
MkSwitch,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('profile'),
icon: faUser
}]
},
host,
name: null,
description: null,
birthday: null,
location: null,
fieldName0: null,
fieldValue0: null,
fieldName1: null,
fieldValue1: null,
fieldName2: null,
fieldValue2: null,
fieldName3: null,
fieldValue3: null,
avatarId: null,
bannerId: null,
isBot: false,
isCat: false,
saving: false,
faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake
}
},
created() {
this.name = this.$store.state.i.name;
this.description = this.$store.state.i.description;
this.location = this.$store.state.i.location;
this.birthday = this.$store.state.i.birthday;
this.avatarId = this.$store.state.i.avatarId;
this.bannerId = this.$store.state.i.bannerId;
this.isBot = this.$store.state.i.isBot;
this.isCat = this.$store.state.i.isCat;
this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
this.fieldName1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].name : null;
this.fieldValue1 = this.$store.state.i.fields[1] ? this.$store.state.i.fields[1].value : null;
this.fieldName2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].name : null;
this.fieldValue2 = this.$store.state.i.fields[2] ? this.$store.state.i.fields[2].value : null;
this.fieldName3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].name : null;
this.fieldValue3 = this.$store.state.i.fields[3] ? this.$store.state.i.fields[3].value : null;
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
changeAvatar(e) {
selectFile(e.currentTarget || e.target, this.$t('avatar')).then(file => {
os.api('i/update', {
avatarId: file.id,
});
});
},
changeBanner(e) {
selectFile(e.currentTarget || e.target, this.$t('banner')).then(file => {
os.api('i/update', {
bannerId: file.id,
});
});
},
save(notify) {
const fields = [
{ name: this.fieldName0, value: this.fieldValue0 },
{ name: this.fieldName1, value: this.fieldValue1 },
{ name: this.fieldName2, value: this.fieldValue2 },
{ name: this.fieldName3, value: this.fieldValue3 },
];
this.saving = true;
os.api('i/update', {
name: this.name || null,
description: this.description || null,
location: this.location || null,
birthday: this.birthday || null,
fields,
isBot: !!this.isBot,
isCat: !!this.isCat,
}).then(i => {
this.saving = false;
this.$store.state.i.avatarId = i.avatarId;
this.$store.state.i.avatarUrl = i.avatarUrl;
this.$store.state.i.bannerId = i.bannerId;
this.$store.state.i.bannerUrl = i.bannerUrl;
if (notify) {
os.success();
}
}).catch(err => {
this.saving = false;
os.dialog({
type: 'error',
text: err.id
});
});
},
}
});
</script>
<style lang="scss" scoped>
.llvierxe {
> ._content {
> .header {
position: relative;
height: 150px;
overflow: hidden;
background-size: cover;
background-position: center;
border-radius: 5px;
border: solid 1px var(--divider);
box-sizing: border-box;
cursor: pointer;
> .avatar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: block;
width: 72px;
height: 72px;
margin: auto;
cursor: pointer;
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
}
}
> .fields {
> .row {
> * {
display: inline-block;
width: 50%;
margin-bottom: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="_section">
<div class="_card">
<div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
<div class="_content">
<MkInput v-model:value="reactions" style="font-family: 'Segoe UI Emoji', 'Noto Color Emoji', Roboto, HelveticaNeue, Arial, sans-serif">
{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></template>
</MkInput>
<MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton>
</div>
<div class="_footer">
<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
<MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import MkInput from '@/components/ui/input.vue';
import MkButton from '@/components/ui/button.vue';
import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
import { defaultSettings } from '@/store';
import * as os from '@/os';
export default defineComponent({
components: {
MkInput,
MkButton,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('reaction'),
icon: faLaugh
}]
},
reactions: this.$store.state.settings.reactions.join(''),
changed: false,
faLaugh, faSave, faEye, faUndo
}
},
computed: {
splited(): any {
return this.reactions.match(emojiRegexWithCustom);
},
},
watch: {
reactions: {
handler() {
this.changed = true;
},
deep: true
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
save() {
this.$store.dispatch('settings/set', { key: 'reactions', value: this.splited });
this.changed = false;
},
async preview(ev) {
os.popup(await import('@/components/reaction-picker.vue'), {
reactions: this.splited,
showFocus: false,
src: ev.currentTarget || ev.target,
}, {}, 'closed');
},
setDefault() {
this.reactions = defaultSettings.reactions.join('');
},
async chooseEmoji(ev) {
os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
this.reactions += emoji;
});
}
}
});
</script>

View File

@@ -0,0 +1,235 @@
<template>
<section class="_card">
<div class="_title"><Fa :icon="faLock"/> {{ $t('twoStepAuthentication') }}</div>
<div class="_content">
<MkButton v-if="!data && !$store.state.i.twoFactorEnabled" @click="register">{{ $t('_2fa.registerDevice') }}</MkButton>
<template v-if="$store.state.i.twoFactorEnabled">
<p>{{ $t('_2fa.alreadyRegistered') }}</p>
<MkButton @click="unregister">{{ $t('unregister') }}</MkButton>
<template v-if="supportsCredentials">
<hr class="totp-method-sep">
<h2 class="heading">{{ $t('securityKey') }}</h2>
<p>{{ $t('_2fa.securityKeyInfo') }}</p>
<div class="key-list">
<div class="key" v-for="key in $store.state.i.securityKeysList">
<h3>{{ key.name }}</h3>
<div class="last-used">{{ $t('lastUsed') }}<MkTime :time="key.lastUsed"/></div>
<MkButton @click="unregisterKey(key)">{{ $t('unregister') }}</MkButton>
</div>
</div>
<MkSwitch v-model:value="usePasswordLessLogin" @update:value="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</MkSwitch>
<MkInfo warn v-if="registration && registration.error">{{ $t('error') }} {{ registration.error }}</MkInfo>
<MkButton v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</MkButton>
<ol v-if="registration && !registration.error">
<li v-if="registration.stage >= 0">
{{ $t('tapSecurityKey') }}
<Fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
</li>
<li v-if="registration.stage >= 1">
<MkForm :disabled="registration.stage != 1 || registration.saving">
<MkInput v-model:value="keyName" :max="30">
<span>{{ $t('securityKeyName') }}</span>
</MkInput>
<MkButton @click="registerKey" :disabled="keyName.length == 0">{{ $t('registerSecurityKey') }}</MkButton>
<Fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
</MkForm>
</li>
</ol>
</template>
</template>
<div v-if="data && !$store.state.i.twoFactorEnabled">
<ol style="margin: 0; padding: 0 0 0 1em;">
<li>
<i18n-t keypath="_2fa.step1" tag="span">
<template #a>
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
</template>
<template #b>
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
</template>
</i18n-t>
</li>
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
<li>{{ $t('_2fa.step3') }}<br>
<MkInput v-model:value="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false">{{ $t('token') }}</MkInput>
<MkButton primary @click="submit">{{ $t('done') }}</MkButton>
</li>
</ol>
<MkInfo>{{ $t('_2fa.step4') }}</MkInfo>
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { hostname } from '@/config';
import { byteify, hexify, stringify } from '@/scripts/2fa';
import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/ui/input.vue';
import MkSwitch from '@/components/ui/switch.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton, MkInfo, MkInput, MkSwitch
},
data() {
return {
data: null,
supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
registration: null,
keyName: '',
token: null,
faLock
};
},
methods: {
register() {
os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/2fa/register', {
password: password
}).then(data => {
this.data = data;
});
});
},
unregister() {
os.dialog({
title: this.$t('password'),
input: {
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.$store.state.i.twoFactorEnabled = false;
});
});
},
submit() {
os.api('i/2fa/done', {
token: this.token
}).then(() => {
os.success();
this.$store.state.i.twoFactorEnabled = true;
}).catch(e => {
os.dialog({
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.dialog({
title: this.$t('password'),
input: {
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.dialog({
title: this.$t('password'),
input: {
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.$store.state.i.id, 'ascii'),
name: this.$store.state.i.username,
displayName: this.$store.state.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
});
}
}
});
</script>

View File

@@ -0,0 +1,102 @@
<template>
<div>
<div class="_section">
<X2fa/>
</div>
<div class="_section">
<MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton>
</div>
<div class="_section">
<MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton>
<div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import X2fa from './security.2fa.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
X2fa,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('security'),
icon: faLock
}]
},
faLock, faSyncAlt
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
async change() {
const { canceled: canceled1, result: currentPassword } = await os.dialog({
title: this.$t('currentPassword'),
input: {
type: 'password'
}
});
if (canceled1) return;
const { canceled: canceled2, result: newPassword } = await os.dialog({
title: this.$t('newPassword'),
input: {
type: 'password'
}
});
if (canceled2) return;
const { canceled: canceled3, result: newPassword2 } = await os.dialog({
title: this.$t('newPasswordRetype'),
input: {
type: 'password'
}
});
if (canceled3) return;
if (newPassword !== newPassword2) {
os.dialog({
type: 'error',
text: this.$t('retypedNotMatch')
});
return;
}
os.apiWithDialog('i/change-password', {
currentPassword,
newPassword
});
},
regenerateToken() {
os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/regenerate_token', {
password: password
});
});
},
}
});
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="_section">
<div class="_card">
<div class="_content">
<MkTextarea v-model:value="items" tall>
<span>{{ $t('sidebar') }}</span>
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
</MkTextarea>
</div>
<div class="_content">
<div>{{ $t('display') }}</div>
<MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio>
<MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio>
<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
</div>
<div class="_footer">
<MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
<MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkRadio from '@/components/ui/radio.vue';
import { defaultDeviceUserSettings } from '@/store';
import * as os from '@/os';
import { sidebarDef } from '@/sidebar';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkRadio,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('sidebar'),
icon: faListUl
}]
},
menuDef: sidebarDef,
items: '',
faSave, faRedo
}
},
computed: {
splited(): string[] {
return this.items.trim().split('\n').filter(x => x.trim() !== '');
},
sidebarDisplay: {
get() { return this.$store.state.device.sidebarDisplay; },
set(value) { this.$store.commit('device/set', { key: 'sidebarDisplay', value }); }
},
},
created() {
this.items = this.$store.state.deviceUser.menu.join('\n');
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
async addItem() {
const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.deviceUser.menu.includes(k));
const { canceled, result: item } = await os.dialog({
type: null,
title: this.$t('addItem'),
select: {
items: [...menu.map(k => ({
value: k, text: this.$t(this.menuDef[k].title)
})), ...[{
value: '-', text: this.$t('divider')
}]]
},
showCancelButton: true
});
if (canceled) return;
this.items = [...this.splited, item].join('\n');
this.save();
},
save() {
this.$store.commit('deviceUser/setMenu', this.splited);
},
reset() {
this.$store.commit('deviceUser/setMenu', defaultDeviceUserSettings.menu);
this.items = this.$store.state.deviceUser.menu.join('\n');
},
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,152 @@
<template>
<div class="_section">
<div class="_card">
<div class="_title"><Fa :icon="faMusic"/> {{ $t('sounds') }}</div>
<div class="_content">
<MkRange v-model:value="sfxVolume" :min="0" :max="1" :step="0.1">
<Fa slot="icon" :icon="volumeIcon"/>
<span slot="title">{{ $t('volume') }}</span>
</MkRange>
</div>
<div class="_content">
<MkSelect v-model:value="sfxNote">
<template #label>{{ $t('_sfx.note') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNote)" v-if="sfxNote"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxNoteMy">
<template #label>{{ $t('_sfx.noteMy') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNoteMy)" v-if="sfxNoteMy"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxNotification">
<template #label>{{ $t('_sfx.notification') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNotification)" v-if="sfxNotification"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChat">
<template #label>{{ $t('_sfx.chat') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChat)" v-if="sfxChat"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChatBg">
<template #label>{{ $t('_sfx.chatBg') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChatBg)" v-if="sfxChatBg"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxAntenna">
<template #label>{{ $t('_sfx.antenna') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChannel">
<template #label>{{ $t('_sfx.channel') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue';
import MkRange from '@/components/ui/range.vue';
import * as os from '@/os';
const sounds = [
null,
'syuilo/up',
'syuilo/down',
'syuilo/pope1',
'syuilo/pope2',
'syuilo/waon',
'syuilo/popo',
'syuilo/triple',
'syuilo/poi1',
'syuilo/poi2',
'syuilo/pirori',
'syuilo/pirori-wet',
'syuilo/pirori-square-wet',
'syuilo/square-pico',
'syuilo/reverved',
'syuilo/ryukyu',
'aisha/1',
'aisha/2',
'aisha/3',
'noizenecio/kick_gaba',
'noizenecio/kick_gaba2',
];
export default defineComponent({
components: {
MkSelect,
MkRange,
},
data() {
return {
sounds,
faMusic, faPlay, faVolumeUp, faVolumeMute,
}
},
computed: {
sfxVolume: {
get() { return this.$store.state.device.sfxVolume; },
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
},
sfxNote: {
get() { return this.$store.state.device.sfxNote; },
set(value) { this.$store.commit('device/set', { key: 'sfxNote', value }); }
},
sfxNoteMy: {
get() { return this.$store.state.device.sfxNoteMy; },
set(value) { this.$store.commit('device/set', { key: 'sfxNoteMy', value }); }
},
sfxNotification: {
get() { return this.$store.state.device.sfxNotification; },
set(value) { this.$store.commit('device/set', { key: 'sfxNotification', value }); }
},
sfxChat: {
get() { return this.$store.state.device.sfxChat; },
set(value) { this.$store.commit('device/set', { key: 'sfxChat', value }); }
},
sfxChatBg: {
get() { return this.$store.state.device.sfxChatBg; },
set(value) { this.$store.commit('device/set', { key: 'sfxChatBg', value }); }
},
sfxAntenna: {
get() { return this.$store.state.device.sfxAntenna; },
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
},
sfxChannel: {
get() { return this.$store.state.device.sfxChannel; },
set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
},
volumeIcon: {
get() {
return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
}
}
},
methods: {
listen(sound) {
const audio = new Audio(`/assets/sounds/${sound}.mp3`);
audio.volume = this.$store.state.device.sfxVolume;
audio.play();
},
}
});
</script>

View File

@@ -0,0 +1,499 @@
<template>
<div class="_section">
<div class="rfqxtzch _card _vMargin">
<div class="_content">
<div class="darkMode" :class="{ disabled: syncDeviceDarkMode }">
<div class="toggleWrapper">
<input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/>
<label for="dn" class="toggle">
<span class="before">{{ $t('light') }}</span>
<span class="after">{{ $t('dark') }}</span>
<span class="toggle__handler">
<span class="crater crater--1"></span>
<span class="crater crater--2"></span>
<span class="crater crater--3"></span>
</span>
<span class="star star--1"></span>
<span class="star star--2"></span>
<span class="star star--3"></span>
<span class="star star--4"></span>
<span class="star star--5"></span>
<span class="star star--6"></span>
</label>
</div>
</div>
<MkSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</MkSwitch>
</div>
<div class="_content">
<MkSelect v-model:value="lightTheme">
<template #label>{{ $t('themeForLightMode') }}</template>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model:value="darkTheme">
<template #label>{{ $t('themeForDarkMode') }}</template>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</MkSelect>
<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a><router-link to="/theme-editor" class="_link">{{ $t('_theme.make') }}</router-link>
</div>
<div class="_content">
<MkButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</MkButton>
<MkButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</MkButton>
</div>
</div>
<div class="_card _vMargin">
<div class="_title"><Fa :icon="faDownload"/> {{ $t('_theme.install') }}</div>
<div class="_content">
<MkTextarea v-model:value="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</MkTextarea>
<MkButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</MkButton>
<MkButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
</div>
</div>
<div class="_card _vMargin">
<div class="_title"><Fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</div>
<div class="_content">
<MkSelect v-model:value="selectedThemeId">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</MkSelect>
<template v-if="selectedTheme">
<MkTextarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
</MkTextarea>
<MkButton @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
</template>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import { Theme, builtinThemes, applyTheme, validateTheme } from '@/scripts/theme';
import { selectFile } from '@/scripts/select-file';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSelect,
MkSwitch,
MkTextarea,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('theme'),
icon: faPalette
}]
},
builtinThemes,
installThemeCode: null,
selectedThemeId: null,
wallpaper: localStorage.getItem('wallpaper'),
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
computed: {
themes(): Theme[] {
return builtinThemes.concat(this.$store.state.device.themes);
},
installedThemes(): Theme[] {
return this.$store.state.device.themes;
},
darkThemes(): Theme[] {
return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
},
lightThemes(): Theme[] {
return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
},
darkTheme: {
get() { return this.$store.state.device.darkTheme; },
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
},
lightTheme: {
get() { return this.$store.state.device.lightTheme; },
set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
},
darkMode: {
get() { return this.$store.state.device.darkMode; },
set(value) { this.$store.commit('device/set', { key: 'darkMode', value }); }
},
syncDeviceDarkMode: {
get() { return this.$store.state.device.syncDeviceDarkMode; },
set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
},
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
},
watch: {
darkTheme() {
if (this.$store.state.device.darkMode) {
applyTheme(this.themes.find(x => x.id === this.darkTheme));
}
},
lightTheme() {
if (!this.$store.state.device.darkMode) {
applyTheme(this.themes.find(x => x.id === this.lightTheme));
}
},
syncDeviceDarkMode() {
if (this.$store.state.device.syncDeviceDarkMode) {
this.$store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
}
},
wallpaper() {
if (this.wallpaper == null) {
localStorage.removeItem('wallpaper');
} else {
localStorage.setItem('wallpaper', this.wallpaper);
}
location.reload();
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
setWallpaper(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {
this.wallpaper = file.url;
});
},
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
os.success();
},
parseThemeCode(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
os.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
return false;
}
return theme;
},
preview(code) {
const theme = this.parseThemeCode(code);
if (theme) applyTheme(theme, false);
},
install(code) {
const theme = this.parseThemeCode(code);
if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
},
uninstall() {
const theme = this.selectedTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.success();
},
}
});
</script>
<style lang="scss" scoped>
.rfqxtzch {
> ._content {
> .darkMode {
position: relative;
padding: 32px 0;
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
}
.toggleWrapper {
position: absolute;
top: 50%;
left: 50%;
overflow: hidden;
padding: 0 100px;
transform: translate3d(-50%, -50%, 0);
input {
position: absolute;
left: -99em;
}
}
.toggle {
cursor: pointer;
display: inline-block;
position: relative;
width: 90px;
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
> .before, > .after {
position: absolute;
top: 15px;
font-size: 18px;
transition: color 1s ease;
}
> .before {
left: -70px;
color: var(--accent);
}
> .after {
right: -68px;
color: var(--fg);
}
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
top: 3px;
left: 3px;
width: 50px - 6;
height: 50px - 6;
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out !important;
border-radius: 100%;
}
.crater--1 {
top: 18px;
left: 10px;
width: 4px;
height: 4px;
}
.crater--2 {
top: 28px;
left: 22px;
width: 6px;
height: 6px;
}
.crater--3 {
top: 10px;
left: 25px;
width: 8px;
height: 8px;
}
}
.star {
position: absolute;
background-color: #ffffff;
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
border-radius: 50%;
}
.star--1 {
top: 10px;
left: 35px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--2 {
top: 18px;
left: 28px;
z-index: 1;
width: 30px;
height: 3px;
}
.star--3 {
top: 27px;
left: 40px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--4,
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--4 {
top: 16px;
left: 11px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
.star--5 {
top: 32px;
left: 17px;
z-index: 0;
width: 3px;
height: 3px;
transform: translate3d(3px,0,0);
}
.star--6 {
top: 36px;
left: 28px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
> .before {
color: var(--fg);
}
> .after {
color: var(--accent);
}
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="_section">
<div class="_card">
<MkTab v-model:value="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
<div class="_content">
<div v-show="tab === 'soft'">
<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>
<MkTextarea v-model:value="softMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</MkTextarea>
</div>
<div v-show="tab === 'hard'">
<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo>
<MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</MkTextarea>
<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div>
</div>
</div>
<div class="_footer">
<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkTab from '@/components/tab.vue';
import MkInfo from '@/components/ui/info.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkTab,
MkInfo,
},
emits: ['info'],
data() {
return {
INFO: {
header: [{
title: this.$t('wordMute'),
icon: faCommentSlash
}]
},
tab: 'soft',
softMutedWords: '',
hardMutedWords: '',
hardWordMutedNotesCount: null,
changed: false,
faSave,
}
},
watch: {
softMutedWords: {
handler() {
this.changed = true;
},
deep: true
},
hardMutedWords: {
handler() {
this.changed = true;
},
deep: true
},
},
async created() {
this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
async save() {
this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
await os.api('i/update', {
mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
});
this.changed = false;
},
}
});
</script>