マルチアカウント対応
This commit is contained in:
parent
24b419fe60
commit
8a5daa866c
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misshaialert",
|
"name": "misskey-tools",
|
||||||
"version": "1.5.1",
|
"version": "2.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "built/app.js",
|
"main": "built/app.js",
|
||||||
"author": "Xeltica",
|
"author": "Xeltica",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
version: '1.5.1',
|
version: '2.0.0',
|
||||||
changelog: [
|
changelog: [
|
||||||
'インスタンスの接続エラーにより後続処理が行えなくなる重大な不具合を修正',
|
'インスタンスの接続エラーにより後続処理が行えなくなる重大な不具合を修正',
|
||||||
'全員分の算出が終わるまで、ランキングを非表示に',
|
'全員分の算出が終わるまで、ランキングを非表示に',
|
||||||
|
@ -31,7 +31,14 @@ html
|
|||||||
|
|
||||||
if token
|
if token
|
||||||
script.
|
script.
|
||||||
localStorage.setItem('token', '#{token}');
|
const token = '#{token}';
|
||||||
|
const previousToken = localStorage.getItem('token');
|
||||||
|
const accounts = JSON.parse(localStorage.getItem('accounts') && '[]');
|
||||||
|
if (previousToken && !accounts.includes(previousToken)) {
|
||||||
|
accounts.push(previousToken);
|
||||||
|
}
|
||||||
|
localStorage.setItem('accounts', JSON.stringify(accounts));
|
||||||
|
localStorage.setItem('token', token);
|
||||||
history.replaceState(null, null, '/');
|
history.replaceState(null, null, '/');
|
||||||
|
|
||||||
if error
|
if error
|
||||||
|
67
src/frontend/components/AccountsPage.tsx
Normal file
67
src/frontend/components/AccountsPage.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
|
||||||
|
import { useGetSessionQuery } from '../services/session';
|
||||||
|
import { useSelector } from '../store';
|
||||||
|
import { setAccounts } from '../store/slices/screen';
|
||||||
|
import { LoginForm } from './LoginForm';
|
||||||
|
import { Skeleton } from './Skeleton';
|
||||||
|
|
||||||
|
export const AccountsPage: React.VFC = () => {
|
||||||
|
const {data} = useGetSessionQuery(undefined);
|
||||||
|
const {t} = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {accounts, accountTokens} = useSelector(state => state.screen);
|
||||||
|
|
||||||
|
const switchAccount = (token: string) => {
|
||||||
|
const newAccounts = accountTokens.filter(a => a !== token);
|
||||||
|
newAccounts.push(localStorage.getItem(LOCALSTORAGE_KEY_TOKEN) ?? '');
|
||||||
|
localStorage.setItem(LOCALSTORAGE_KEY_ACCOUNTS, JSON.stringify(newAccounts));
|
||||||
|
localStorage.setItem(LOCALSTORAGE_KEY_TOKEN, token);
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return !data ? (
|
||||||
|
<div className="vstack">
|
||||||
|
<Skeleton />
|
||||||
|
<Skeleton />
|
||||||
|
<Skeleton />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="fade vstack">
|
||||||
|
<div className="card">
|
||||||
|
<div className="body">
|
||||||
|
<h1>{t('_accounts.currentAccount')}</h1>
|
||||||
|
<p>@{data.username}@{data.host}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<article>
|
||||||
|
<h2>{t('_accounts.switchAccount')}</h2>
|
||||||
|
<div className="menu large fluid mb-2">
|
||||||
|
{
|
||||||
|
accounts.length === accountTokens.length ? (
|
||||||
|
accounts.map(account => (
|
||||||
|
<button className="item fluid" style={{display: 'flex', flexDirection: 'row', alignItems: 'center'}} onClick={() => switchAccount(account.misshaiToken)}>
|
||||||
|
<i className="icon bi bi-chevron-right" />
|
||||||
|
@{account.username}@{account.host}
|
||||||
|
<button className="btn flat text-danger" style={{marginLeft: 'auto'}} onClick={e => {
|
||||||
|
const filteredAccounts = accounts.filter(ac => ac.id !== account.id);
|
||||||
|
dispatch(setAccounts(filteredAccounts));
|
||||||
|
e.stopPropagation();
|
||||||
|
}}>
|
||||||
|
<i className="bi bi-trash"/>
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="item">...</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<LoginForm />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -10,7 +10,7 @@ export const LoginForm: React.VFC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<strong>{t('instanceUrl')}</strong>
|
<strong>{t('instanceUrl')}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="hgroup">
|
<div className="hgroup login-form">
|
||||||
<input
|
<input
|
||||||
className="input-field"
|
className="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const LOCALSTORAGE_KEY_TOKEN = 'token';
|
export const LOCALSTORAGE_KEY_TOKEN = 'token';
|
||||||
export const LOCALSTORAGE_KEY_THEME = 'theme';
|
export const LOCALSTORAGE_KEY_THEME = 'theme';
|
||||||
export const LOCALSTORAGE_KEY_LANG = 'lang';
|
export const LOCALSTORAGE_KEY_LANG = 'lang';
|
||||||
|
export const LOCALSTORAGE_KEY_ACCOUNTS = 'accounts';
|
||||||
|
|
||||||
export const API_ENDPOINT = `//${location.host}/api/v1/`;
|
export const API_ENDPOINT = `//${location.host}/api/v1/`;
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
},
|
},
|
||||||
"_nav": {
|
"_nav": {
|
||||||
"misshai": "ミス廃",
|
"misshai": "ミス廃",
|
||||||
|
"accounts": "アカウント",
|
||||||
"settings": "設定"
|
"settings": "設定"
|
||||||
},
|
},
|
||||||
"_missHai": {
|
"_missHai": {
|
||||||
@ -73,6 +74,11 @@
|
|||||||
"order": "順位",
|
"order": "順位",
|
||||||
"showRanking": "ランキングを見る"
|
"showRanking": "ランキングを見る"
|
||||||
},
|
},
|
||||||
|
"_accounts": {
|
||||||
|
"currentAccount": "現在ログインしているアカウント",
|
||||||
|
"switchAccount": "アカウント切り替え",
|
||||||
|
"useAnother": "他のアカウントで登録する"
|
||||||
|
},
|
||||||
"_developerInfo": {
|
"_developerInfo": {
|
||||||
"title": "開発者",
|
"title": "開発者",
|
||||||
"description": "何か困ったことがあったら、以下のアカウントにメッセージを送ってください。"
|
"description": "何か困ったことがあったら、以下のアカウントにメッセージを送ってください。"
|
||||||
|
@ -1,25 +1,47 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Header } from '../components/Header';
|
import { Header } from '../components/Header';
|
||||||
import { MisshaiPage } from '../components/MisshaiPage';
|
import { MisshaiPage } from '../components/MisshaiPage';
|
||||||
import { Tab, TabItem } from '../components/Tab';
|
import { Tab, TabItem } from '../components/Tab';
|
||||||
import { SettingPage } from '../components/SettingPage';
|
import { SettingPage } from '../components/SettingPage';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AccountsPage } from '../components/AccountsPage';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { API_ENDPOINT, LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
|
||||||
|
import { IUser } from '../../common/types/user';
|
||||||
|
import { setAccounts } from '../store/slices/screen';
|
||||||
|
|
||||||
|
const getSession = (token: string) => {
|
||||||
|
return fetch(`${API_ENDPOINT}session`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}).then(r => r.json()).then(r => r as IUser);
|
||||||
|
};
|
||||||
|
|
||||||
export const IndexSessionPage: React.VFC = () => {
|
export const IndexSessionPage: React.VFC = () => {
|
||||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||||
const {t, i18n} = useTranslation();
|
const {t, i18n} = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
|
||||||
|
Promise.all(accounts.map(getSession)).then(a => dispatch(setAccounts(a)));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const items = useMemo<TabItem[]>(() => ([
|
const items = useMemo<TabItem[]>(() => ([
|
||||||
{ label: t('_nav.misshai') },
|
{ label: t('_nav.misshai') },
|
||||||
|
{ label: t('_nav.accounts') },
|
||||||
{ label: t('_nav.settings') },
|
{ label: t('_nav.settings') },
|
||||||
]), [i18n.language]);
|
]), [i18n.language]);
|
||||||
|
|
||||||
const component = useMemo(() => {
|
const component = useMemo(() => {
|
||||||
switch (selectedTab) {
|
switch (selectedTab) {
|
||||||
case 0: return <MisshaiPage />;
|
case 0: return <MisshaiPage />;
|
||||||
case 1: return <SettingPage/>;
|
case 1: return <AccountsPage />;
|
||||||
|
case 2: return <SettingPage/>;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}, [selectedTab]);
|
}, [selectedTab]);
|
||||||
|
@ -35,7 +35,7 @@ export const IndexWelcomePage: React.VFC = () => {
|
|||||||
<article>
|
<article>
|
||||||
<h3>{t('_welcome.misshaiAlertTitle')}</h3>
|
<h3>{t('_welcome.misshaiAlertTitle')}</h3>
|
||||||
<p>{t('_welcome.misshaiAlertDescription')}</p>
|
<p>{t('_welcome.misshaiAlertDescription')}</p>
|
||||||
<div className="card ma-2 shadow-2" style={{maxWidth: 320}}>
|
<div className="card ma-2 shadow-2" style={{maxWidth: 360}}>
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<pre>{example}</pre>
|
<pre>{example}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
import { IUser } from '../../../common/types/user';
|
||||||
|
|
||||||
import { LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
||||||
import { Theme } from '../../misc/theme';
|
import { Theme } from '../../misc/theme';
|
||||||
import { Modal } from '../../modal/modal';
|
import { Modal } from '../../modal/modal';
|
||||||
|
|
||||||
@ -10,6 +11,8 @@ interface ScreenState {
|
|||||||
modalShown: boolean;
|
modalShown: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
language: string;
|
language: string;
|
||||||
|
accounts: IUser[];
|
||||||
|
accountTokens: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ScreenState = {
|
const initialState: ScreenState = {
|
||||||
@ -17,6 +20,8 @@ const initialState: ScreenState = {
|
|||||||
modalShown: false,
|
modalShown: false,
|
||||||
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
|
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
|
||||||
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
|
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
|
||||||
|
accounts: [],
|
||||||
|
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const screenSlice = createSlice({
|
export const screenSlice = createSlice({
|
||||||
@ -40,9 +45,14 @@ export const screenSlice = createSlice({
|
|||||||
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
||||||
i18n.changeLanguage(action.payload);
|
i18n.changeLanguage(action.payload);
|
||||||
},
|
},
|
||||||
|
setAccounts: (state, action: PayloadAction<ScreenState['accounts']>) => {
|
||||||
|
state.accounts = action.payload;
|
||||||
|
state.accountTokens = action.payload.map(a => a.misshaiToken);
|
||||||
|
localStorage[LOCALSTORAGE_KEY_ACCOUNTS] = JSON.stringify(state.accountTokens);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { showModal, hideModal, changeTheme, changeLang } = screenSlice.actions;
|
export const { showModal, hideModal, changeTheme, changeLang, setAccounts } = screenSlice.actions;
|
||||||
|
|
||||||
export default screenSlice.reducer;
|
export default screenSlice.reducer;
|
||||||
|
@ -92,3 +92,8 @@ small {
|
|||||||
padding: calc(var(--margin) / 2);
|
padding: calc(var(--margin) / 2);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user