Merge branch 'develop' into more-share-page-querys
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { del, get, set } from '@client/scripts/idb-proxy';
|
||||
import { reactive } from 'vue';
|
||||
import { apiUrl } from '@client/config';
|
||||
import { waiting } from '@client/os';
|
||||
import { unisonReload } from '@client/scripts/unison-reload';
|
||||
import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -10,6 +11,7 @@ type Account = {
|
||||
token: string;
|
||||
isModerator: boolean;
|
||||
isAdmin: boolean;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
const data = localStorage.getItem('account');
|
||||
@@ -17,22 +19,57 @@ const data = localStorage.getItem('account');
|
||||
// TODO: 外部からはreadonlyに
|
||||
export const $i = data ? reactive(JSON.parse(data) as Account) : null;
|
||||
|
||||
export function signout() {
|
||||
export async function signout() {
|
||||
waiting();
|
||||
localStorage.removeItem('account');
|
||||
|
||||
//#region Remove account
|
||||
const accounts = await getAccounts();
|
||||
accounts.splice(accounts.findIndex(x => x.id === $i.id), 1);
|
||||
|
||||
if (accounts.length > 0) await set('accounts', accounts);
|
||||
else await del('accounts');
|
||||
//#endregion
|
||||
|
||||
//#region Remove service worker registration
|
||||
try {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const push = await registration.pushManager.getSubscription();
|
||||
if (push) {
|
||||
await fetch(`${apiUrl}/sw/unregister`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: $i.token,
|
||||
endpoint: push.endpoint,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (accounts.length === 0) {
|
||||
await navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
return Promise.all(registrations.map(registration => registration.unregister()));
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
//#endregion
|
||||
|
||||
document.cookie = `igi=; path=/`;
|
||||
location.href = '/';
|
||||
|
||||
if (accounts.length > 0) login(accounts[0].token);
|
||||
else unisonReload();
|
||||
}
|
||||
|
||||
export function getAccounts() {
|
||||
const accountsData = localStorage.getItem('accounts');
|
||||
const accounts: { id: Account['id'], token: Account['token'] }[] = accountsData ? JSON.parse(accountsData) : [];
|
||||
return accounts;
|
||||
export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
|
||||
return (await get('accounts')) || [];
|
||||
}
|
||||
|
||||
export function addAccount(id: Account['id'], token: Account['token']) {
|
||||
const accounts = getAccounts();
|
||||
export async function addAccount(id: Account['id'], token: Account['token']) {
|
||||
const accounts = await getAccounts();
|
||||
if (!accounts.some(x => x.id === id)) {
|
||||
localStorage.setItem('accounts', JSON.stringify(accounts.concat([{ id, token }])));
|
||||
await set('accounts', accounts.concat([{ id, token }]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +84,7 @@ function fetchAccount(token): Promise<Account> {
|
||||
})
|
||||
.then(res => {
|
||||
// When failed to authenticate user
|
||||
if (res.status >= 400 && res.status < 500) {
|
||||
if (res.status !== 200 && res.status < 500) {
|
||||
return signout();
|
||||
}
|
||||
|
||||
@@ -69,15 +106,22 @@ export function updateAccount(data) {
|
||||
}
|
||||
|
||||
export function refreshAccount() {
|
||||
fetchAccount($i.token).then(updateAccount);
|
||||
return fetchAccount($i.token).then(updateAccount);
|
||||
}
|
||||
|
||||
export async function login(token: Account['token']) {
|
||||
export async function login(token: Account['token'], redirect?: string) {
|
||||
waiting();
|
||||
if (_DEV_) console.log('logging as token ', token);
|
||||
const me = await fetchAccount(token);
|
||||
localStorage.setItem('account', JSON.stringify(me));
|
||||
addAccount(me.id, token);
|
||||
await addAccount(me.id, token);
|
||||
|
||||
if (redirect) {
|
||||
reloadChannel.postMessage('reload');
|
||||
location.href = redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ export default defineComponent({
|
||||
|
||||
&:not(.noGap) {
|
||||
> .notes {
|
||||
background: var(--bg);
|
||||
|
||||
.qtqtichx {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p>
|
||||
|
||||
<div v-else>
|
||||
<XList class="notifications" :items="items" v-slot="{ item: notification }" :no-gap="true">
|
||||
<XList class="elsfgstc" :items="items" v-slot="{ item: notification }" :no-gap="true">
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/>
|
||||
<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
|
||||
</XList>
|
||||
@@ -141,4 +141,8 @@ export default defineComponent({
|
||||
text-align: center;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.elsfgstc {
|
||||
background: var(--panel);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -367,7 +367,12 @@ export default defineComponent({
|
||||
this.cw = init.cw;
|
||||
this.useCw = init.cw != null;
|
||||
if (init.poll) {
|
||||
this.poll = init.poll;
|
||||
this.poll = {
|
||||
choices: init.poll.choices.map(x => x.text),
|
||||
multiple: init.poll.multiple,
|
||||
expiresAt: init.poll.expiresAt,
|
||||
expiredAfter: init.poll.expiredAfter,
|
||||
};
|
||||
}
|
||||
this.visibility = init.visibility;
|
||||
this.localOnly = init.localOnly;
|
||||
|
||||
@@ -111,7 +111,9 @@ export default defineComponent({
|
||||
|
||||
onLogin(res) {
|
||||
if (this.autoSet) {
|
||||
login(res.i);
|
||||
return login(res.i);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -144,7 +146,7 @@ export default defineComponent({
|
||||
});
|
||||
}).then(res => {
|
||||
this.$emit('login', res);
|
||||
this.onLogin(res);
|
||||
return this.onLogin(res);
|
||||
}).catch(err => {
|
||||
if (err === null) return;
|
||||
os.dialog({
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
|
||||
<form class="qlvuhzng" @submit.prevent="onSubmit" :autocomplete="Math.random()">
|
||||
<template v-if="meta">
|
||||
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
|
||||
<MkInput class="_inputNoTopMargin" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
|
||||
<template #label>{{ $ts.invitationCode }}</template>
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
|
||||
<template #label>{{ $ts.username }}</template>
|
||||
<MkInput class="_inputNoTopMargin" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
|
||||
<template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
<template #caption>
|
||||
@@ -178,14 +178,14 @@ export default defineComponent({
|
||||
'hcaptcha-response': this.hCaptchaResponse,
|
||||
'g-recaptcha-response': this.reCaptchaResponse,
|
||||
}).then(() => {
|
||||
os.api('signin', {
|
||||
return os.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(res => {
|
||||
this.$emit('signup', res);
|
||||
|
||||
if (this.autoSet) {
|
||||
login(res.i);
|
||||
return login(res.i);
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
@@ -204,7 +204,7 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-signup {
|
||||
.qlvuhzng {
|
||||
.captcha {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Directive, ref } from 'vue';
|
||||
import { isDeviceTouch } from '@client/scripts/is-device-touch';
|
||||
import { popup } from '@client/os';
|
||||
import { popup, dialog } from '@client/os';
|
||||
|
||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||
@@ -24,6 +24,18 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
if (binding.arg === 'dialog') {
|
||||
el.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
dialog({
|
||||
type: 'info',
|
||||
text: binding.value,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const show = e => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (self._close) return;
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
import '@client/style.scss';
|
||||
|
||||
//#region account indexedDB migration
|
||||
import { set } from '@client/scripts/idb-proxy';
|
||||
|
||||
if (localStorage.getItem('accounts') != null) {
|
||||
set('accounts', JSON.parse(localStorage.getItem('accounts')));
|
||||
localStorage.removeItem('accounts');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { computed, createApp, watch, markRaw } from 'vue';
|
||||
@@ -92,15 +101,12 @@ window.addEventListener('resize', () => {
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// Get the <head> element
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
||||
// If mobile, insert the viewport meta tag
|
||||
if (isMobile || window.innerWidth <= 1024) {
|
||||
const viewport = document.getElementsByName('viewport').item(0);
|
||||
viewport.setAttribute('content',
|
||||
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
|
||||
head.appendChild(viewport);
|
||||
document.head.appendChild(viewport);
|
||||
}
|
||||
|
||||
//#region Set lang attr
|
||||
@@ -301,6 +307,13 @@ for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
||||
}
|
||||
|
||||
if ($i) {
|
||||
if ($i.isDeleted) {
|
||||
dialog({
|
||||
type: 'warning',
|
||||
text: i18n.locale.accountDeletionInProgress,
|
||||
});
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
if (Notification.permission === 'default') {
|
||||
|
||||
@@ -214,7 +214,11 @@ export function modalPageWindow(path: string) {
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
export function dialog(props: Record<string, any>) {
|
||||
export function dialog(props: {
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting';
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
popup(import('@client/components/dialog.vue'), props, {
|
||||
done: result => {
|
||||
|
||||
@@ -60,7 +60,7 @@ import FormBase from '@client/components/form/base.vue';
|
||||
import FormGroup from '@client/components/form/group.vue';
|
||||
import FormKeyValueView from '@client/components/form/key-value-view.vue';
|
||||
import MkLink from '@client/components/link.vue';
|
||||
import { physics } from '@client/scripts/physics.ts';
|
||||
import { physics } from '@client/scripts/physics';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
const patrons = [
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option>
|
||||
<option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
|
||||
<option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
DeepL Auth Key
|
||||
</FormInput>
|
||||
<FormSwitch v-model:value="deeplIsPro">
|
||||
Pro account
|
||||
</FormSwitch>
|
||||
</FormGroup>
|
||||
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormSuspense>
|
||||
@@ -50,6 +53,7 @@ export default defineComponent({
|
||||
},
|
||||
summalyProxy: '',
|
||||
deeplAuthKey: '',
|
||||
deeplIsPro: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -62,11 +66,13 @@ export default defineComponent({
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.summalyProxy = meta.summalyProxy;
|
||||
this.deeplAuthKey = meta.deeplAuthKey;
|
||||
this.deeplIsPro = meta.deeplIsPro;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy: this.summalyProxy,
|
||||
deeplAuthKey: this.deeplAuthKey,
|
||||
deeplIsPro: this.deeplIsPro,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<XNotifications class="_content" @before="before" @after="after" page/>
|
||||
<div class="clupoqwt" v-size="{ min: [800] }">
|
||||
<XNotifications class="notifications" @before="before" @after="after" page/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,3 +43,17 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.clupoqwt {
|
||||
&.min-width_800px {
|
||||
background: var(--bg);
|
||||
padding: 32px 0;
|
||||
|
||||
> .notifications {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,10 +48,10 @@ export default defineComponent({
|
||||
title: this.$ts.accounts,
|
||||
icon: 'fas fa-users',
|
||||
},
|
||||
storedAccounts: getAccounts().filter(x => x.id !== this.$i.id),
|
||||
storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)),
|
||||
accounts: null,
|
||||
init: () => os.api('users/show', {
|
||||
userIds: this.storedAccounts.map(x => x.id)
|
||||
init: async () => os.api('users/show', {
|
||||
userIds: (await this.storedAccounts).map(x => x.id)
|
||||
}).then(accounts => {
|
||||
this.accounts = accounts;
|
||||
}),
|
||||
@@ -104,8 +104,8 @@ export default defineComponent({
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
switchAccount(account: any) {
|
||||
const storedAccounts = getAccounts();
|
||||
async switchAccount(account: any) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find(x => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
67
src/client/pages/settings/delete-account.vue
Normal file
67
src/client/pages/settings/delete-account.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
|
||||
<FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo>
|
||||
<FormButton @click="deleteAccount" danger v-if="!$i.isDeleted">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
|
||||
<FormButton disabled v-else>{{ $ts._accountDelete.inProgress }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormInfo from '@client/components/form/info.vue';
|
||||
import FormBase from '@client/components/form/base.vue';
|
||||
import FormGroup from '@client/components/form/group.vue';
|
||||
import FormButton from '@client/components/form/button.vue';
|
||||
import * as os from '@client/os';
|
||||
import { debug } from '@client/config';
|
||||
import { signout } from '@client/account';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormButton,
|
||||
FormGroup,
|
||||
FormInfo,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts._accountDelete.accountDelete,
|
||||
icon: 'fas fa-exclamation-triangle'
|
||||
},
|
||||
debug,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async deleteAccount() {
|
||||
const { canceled, result: password } = await os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('i/delete-account', {
|
||||
password: password
|
||||
});
|
||||
|
||||
await os.dialog({
|
||||
title: this.$ts._accountDelete.started,
|
||||
});
|
||||
|
||||
signout();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -45,6 +45,10 @@
|
||||
</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormSwitch v-model:value="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
|
||||
</FormGroup>
|
||||
|
||||
<FormRadios v-model="fontSize">
|
||||
<template #desc>{{ $ts.fontSize }}</template>
|
||||
<option value="small"><span style="font-size: 14px;">Aa</span></option>
|
||||
@@ -149,6 +153,7 @@ export default defineComponent({
|
||||
enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
|
||||
useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
|
||||
squareAvatars: defaultStore.makeGetterSetter('squareAvatars'),
|
||||
aiChanMode: defaultStore.makeGetterSetter('aiChanMode'),
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -184,6 +189,10 @@ export default defineComponent({
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
aiChanMode() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
showGapBetweenNotesInTimeline() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
@@ -132,6 +132,7 @@ export default defineComponent({
|
||||
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
|
||||
case 'update': return defineAsyncComponent(() => import('./update.vue'));
|
||||
case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
|
||||
case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
|
||||
case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
|
||||
}
|
||||
if (page.value.startsWith('registry/keys/system/')) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
|
||||
<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
|
||||
|
||||
<FormButton @click="closeAccount" danger>{{ $ts.closeAccount }}</FormButton>
|
||||
<FormLink to="./delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,6 @@ import FormButton from '@client/components/form/button.vue';
|
||||
import * as os from '@client/os';
|
||||
import { debug } from '@client/config';
|
||||
import { defaultStore } from '@client/store';
|
||||
import { signout } from '@client/account';
|
||||
import { unisonReload } from '@client/scripts/unison-reload';
|
||||
import * as symbols from '@client/symbols';
|
||||
|
||||
@@ -92,22 +91,6 @@ export default defineComponent({
|
||||
os.popup(import('@client/components/taskmanager.vue'), {
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
closeAccount() {
|
||||
os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/delete-account', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
signout();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</FormSelect>
|
||||
<FormSwitch v-model:value="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch>
|
||||
</FormGroup>
|
||||
<FormSwitch v-model:value="keepCw" @update:value="save()">{{ $ts.keepCw }}</FormSwitch>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
@@ -69,6 +70,7 @@ export default defineComponent({
|
||||
defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
|
||||
defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
|
||||
rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
|
||||
keepCw: defaultStore.makeGetterSetter('keepCw'),
|
||||
},
|
||||
|
||||
created() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="cmuxhskf" v-hotkey.global="keymap">
|
||||
<div class="cmuxhskf" v-hotkey.global="keymap" v-size="{ min: [800] }">
|
||||
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block _isolated"/>
|
||||
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block _isolated" fixed/>
|
||||
<div class="tabs">
|
||||
@@ -19,17 +19,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
|
||||
<XTimeline ref="tl" class="tl"
|
||||
:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
|
||||
:src="src"
|
||||
:list="list ? list.id : null"
|
||||
:antenna="antenna ? antenna.id : null"
|
||||
:channel="channel ? channel.id : null"
|
||||
:sound="true"
|
||||
@before="before()"
|
||||
@after="after()"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
<div class="tl">
|
||||
<XTimeline ref="tl" class="tl"
|
||||
:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
|
||||
:src="src"
|
||||
:list="list ? list.id : null"
|
||||
:antenna="antenna ? antenna.id : null"
|
||||
:channel="channel ? channel.id : null"
|
||||
:sound="true"
|
||||
@before="before()"
|
||||
@after="after()"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -231,6 +233,7 @@ export default defineComponent({
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
// 影の都合上
|
||||
position: relative;
|
||||
@@ -287,8 +290,16 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
> .tl {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
&.min-width_800px {
|
||||
> .tl {
|
||||
background: var(--bg);
|
||||
padding: 32px 0;
|
||||
|
||||
> .tl {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default defineComponent({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
}).then(res => {
|
||||
login(res.i);
|
||||
return login(res.token);
|
||||
}).catch(() => {
|
||||
this.submitting = false;
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Autocomplete {
|
||||
*/
|
||||
private onInput() {
|
||||
const caretPos = this.textarea.selectionStart;
|
||||
const text = this.text.substr(0, caretPos).split('\n').pop();
|
||||
const text = this.text.substr(0, caretPos).split('\n').pop()!;
|
||||
|
||||
const mentionIndex = text.lastIndexOf('@');
|
||||
const hashtagIndex = text.lastIndexOf('#');
|
||||
@@ -83,7 +83,7 @@ export class Autocomplete {
|
||||
|
||||
const isMention = mentionIndex != -1;
|
||||
const isHashtag = hashtagIndex != -1;
|
||||
const isEmoji = emojiIndex != -1;
|
||||
const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
|
||||
let opened = false;
|
||||
|
||||
|
||||
7
src/client/scripts/get-account-from-id.ts
Normal file
7
src/client/scripts/get-account-from-id.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { get } from '@client/scripts/idb-proxy';
|
||||
|
||||
export async function getAccountFromId(id: string) {
|
||||
const accounts = await get('accounts') as { token: string; id: string; }[];
|
||||
if (!accounts) console.log('Accounts are not recorded');
|
||||
return accounts.find(e => e.id === id);
|
||||
}
|
||||
38
src/client/scripts/idb-proxy.ts
Normal file
38
src/client/scripts/idb-proxy.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、
|
||||
// indexedDBが使えない環境ではlocalStorageを使う
|
||||
import {
|
||||
get as iget,
|
||||
set as iset,
|
||||
del as idel,
|
||||
createStore,
|
||||
} from 'idb-keyval';
|
||||
|
||||
const fallbackName = (key: string) => `idbfallback::${key}`;
|
||||
|
||||
let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true;
|
||||
|
||||
if (idbAvailable) {
|
||||
try {
|
||||
await createStore('keyval-store', 'keyval');
|
||||
} catch (e) {
|
||||
console.error('idb open error', e);
|
||||
idbAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.');
|
||||
|
||||
export async function get(key: string) {
|
||||
if (idbAvailable) return iget(key);
|
||||
return JSON.parse(localStorage.getItem(fallbackName(key)));
|
||||
}
|
||||
|
||||
export async function set(key: string, val: any) {
|
||||
if (idbAvailable) return iset(key, val);
|
||||
return localStorage.setItem(fallbackName(key), JSON.stringify(val));
|
||||
}
|
||||
|
||||
export async function del(key: string) {
|
||||
if (idbAvailable) return idel(key);
|
||||
return localStorage.removeItem(fallbackName(key));
|
||||
}
|
||||
@@ -210,6 +210,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: ''
|
||||
},
|
||||
aiChanMode: {
|
||||
where: 'device',
|
||||
default: false
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -156,6 +156,7 @@ hr {
|
||||
|
||||
._button {
|
||||
appearance: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0; // for Safari
|
||||
background: none;
|
||||
@@ -201,6 +202,11 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
._help {
|
||||
color: var(--accent);
|
||||
cursor: help
|
||||
}
|
||||
|
||||
._textButton {
|
||||
@extend ._button;
|
||||
color: var(--accent);
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../*"],
|
||||
"@client/*": ["./*"]
|
||||
"@client/*": ["./*"],
|
||||
"@lib/*": ["../../lib/*"],
|
||||
},
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
|
||||
@@ -135,7 +135,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async openAccountMenu(ev) {
|
||||
const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id);
|
||||
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id));
|
||||
const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
|
||||
|
||||
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
|
||||
@@ -195,8 +195,8 @@ export default defineComponent({
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
switchAccount(account: any) {
|
||||
const storedAccounts = getAccounts();
|
||||
async switchAccount(account: any) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find(x => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async openAccountMenu(ev) {
|
||||
const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id);
|
||||
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id));
|
||||
const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
|
||||
|
||||
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
|
||||
@@ -161,8 +161,8 @@ export default defineComponent({
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
switchAccount(account: any) {
|
||||
const storedAccounts = getAccounts();
|
||||
async switchAccount(account: any) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find(x => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
@@ -121,7 +121,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async openAccountMenu(ev) {
|
||||
const storedAccounts = getAccounts().filter(x => x.id !== this.$i.id);
|
||||
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id));
|
||||
const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
|
||||
|
||||
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
|
||||
@@ -181,8 +181,8 @@ export default defineComponent({
|
||||
}, 'closed');
|
||||
},
|
||||
|
||||
switchAccount(account: any) {
|
||||
const storedAccounts = getAccounts();
|
||||
async switchAccount(account: any) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find(x => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
@@ -54,12 +54,14 @@
|
||||
<XWidgets v-if="widgetsShowing" class="tray"/>
|
||||
</transition>
|
||||
|
||||
<iframe v-if="$store.state.aiChanMode" class="ivnzpscs" ref="live2d" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
|
||||
|
||||
<XCommon/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { instanceName } from '@client/config';
|
||||
import { StickySidebar } from '@client/scripts/sticky-sidebar';
|
||||
import XSidebar from './default.sidebar.vue';
|
||||
@@ -131,6 +133,19 @@ export default defineComponent({
|
||||
this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
|
||||
this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
|
||||
}, { passive: true });
|
||||
|
||||
if (this.$store.state.aiChanMode) {
|
||||
const iframeRect = this.$refs.live2d.getBoundingClientRect();
|
||||
window.addEventListener('mousemove', ev => {
|
||||
this.$refs.live2d.contentWindow.postMessage({
|
||||
type: 'moveCursor',
|
||||
body: {
|
||||
x: ev.clientX - iframeRect.left,
|
||||
y: ev.clientY - iframeRect.top,
|
||||
}
|
||||
}, '*');
|
||||
}, { passive: true });
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -201,6 +216,10 @@ export default defineComponent({
|
||||
}
|
||||
}], e);
|
||||
},
|
||||
|
||||
onAiClick(ev) {
|
||||
//if (this.live2d) this.live2d.click(ev);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -458,5 +477,15 @@ export default defineComponent({
|
||||
overflow: auto;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
> .ivnzpscs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
height: 600px;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
59
src/client/widgets/aichan.vue
Normal file
59
src/client/widgets/aichan.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<MkContainer :naked="props.transparent" :show-header="false">
|
||||
<iframe class="dedjhjmo" ref="live2d" @click="touched" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100"></iframe>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import define from './define';
|
||||
import MkContainer from '@client/components/ui/container.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
const widget = define({
|
||||
name: 'ai',
|
||||
props: () => ({
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
extends: widget,
|
||||
components: {
|
||||
MkContainer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('mousemove', ev => {
|
||||
const iframeRect = this.$refs.live2d.getBoundingClientRect();
|
||||
this.$refs.live2d.contentWindow.postMessage({
|
||||
type: 'moveCursor',
|
||||
body: {
|
||||
x: ev.clientX - iframeRect.left,
|
||||
y: ev.clientY - iframeRect.top,
|
||||
}
|
||||
}, '*');
|
||||
}, { passive: true });
|
||||
},
|
||||
methods: {
|
||||
touched() {
|
||||
//if (this.live2d) this.live2d.changeExpression('gurugurume');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dedjhjmo {
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -19,6 +19,7 @@ export default function(app: App) {
|
||||
app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
|
||||
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
|
||||
app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue')));
|
||||
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
||||
}
|
||||
|
||||
export const widgets = [
|
||||
@@ -40,4 +41,5 @@ export const widgets = [
|
||||
'jobQueue',
|
||||
'button',
|
||||
'aiscript',
|
||||
'aichan',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user