0
0
Fork 0

UI調整など

This commit is contained in:
Xeltica 2022-06-22 22:54:39 +09:00
parent e3178f30aa
commit 7d40708c42
17 changed files with 359 additions and 316 deletions

View file

@ -0,0 +1,18 @@
export const designSystemColors = [
'red',
'vermilion',
'orange',
'yellow',
'lime',
'green',
'teal',
'cyan',
'skyblue',
'blue',
'indigo',
'purple',
'magenta',
'pink',
];
export type DesignSystemColor = typeof designSystemColors[number];

View file

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { IAnnouncement } from '../../common/types/announcement'; import { IAnnouncement } from '../../common/types/announcement';
@ -7,7 +6,6 @@ import { $get } from '../misc/api';
export const AnnouncementList: React.VFC = () => { export const AnnouncementList: React.VFC = () => {
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]); const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
const {t} = useTranslation();
const fetchAllAnnouncements = () => { const fetchAllAnnouncements = () => {
setAnnouncements([]); setAnnouncements([]);
@ -24,8 +22,7 @@ export const AnnouncementList: React.VFC = () => {
return ( return (
<> <>
<h1 className="mb-0"><i className="fas fa-bell"></i> {t('announcements')}</h1> <div className="large menu xmenu fade">
<div className="large menu fade">
{announcements.map(a => ( {announcements.map(a => (
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}> <Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
{a.title} {a.title}

View file

@ -1,26 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export const DeveloperInfo: React.VFC = () => {
const {t} = useTranslation();
return (
<>
<h1><i className="fas fa-circle-question"></i> {t('_developerInfo.title')}</h1>
<p>{t('_developerInfo.description')}</p>
<div className="menu large">
<a className="item" href="http://groundpolis.app/@Lutica" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
Lutica@groundpolis.app
</a>
<a className="item" href="http://misskey.io/@le" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
le@misskey.io
</a>
<a className="item" href="http://twitter.com/@EbiseLutica" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
EbiseLutica@twitter.com
</a>
</div>
</>
);
};

View file

@ -45,9 +45,7 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
) : isError ? ( ) : isError ? (
<div className="alert bg-danger">{t('failedToFetch')}</div> <div className="alert bg-danger">{t('failedToFetch')}</div>
) : response ? ( ) : response ? (
<> response.isCalculating ? (
<aside>{t('registeredUsersCount')}: {response?.userCount}</aside>
{response.isCalculating ? (
<p>{t('isCalculating')}</p> <p>{t('isCalculating')}</p>
) : ( ) : (
<div className="menu large"> <div className="menu large">
@ -61,28 +59,7 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
</a> </a>
))} ))}
</div> </div>
// <table className="table mt-1 fluid"> )
// <thead>
// <tr>
// <th>{t('_missHai.order')}</th>
// <th>{t('name')}</th>
// <th>{t('_missHai.rating')}</th>
// </tr>
// </thead>
// <tbody>
// {response.ranking.map((r, i) => (
// <tr key={i}>
// <td>{i + 1}</td>
// <td>
// {r.username}@{r.host}
// </td>
// <td>{r.rating}</td>
// </tr>
// ))}
// </tbody>
// </table>
)}
</>
) : null ) : null
); );
}; };

View file

@ -1,11 +1,20 @@
/** ローカルストレージキー Misskey Tools API トークン */
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 LOCALSTORAGE_KEY_ACCOUNTS = 'accounts';
/** ローカルストレージキー アクセントカラー設定 */
export const LOCALSTORAGE_KEY_ACCENT_COLOR = 'accent_color';
/** Misskey Tools API エンドポイント */
export const API_ENDPOINT = `//${location.host}/api/v1/`; export const API_ENDPOINT = `//${location.host}/api/v1/`;
/** 更新履歴URL */
export const CHANGELOG_URL = 'https://github.com/Xeltica/MisskeyTools/blob/master/CHANGELOG.md'; export const CHANGELOG_URL = 'https://github.com/Xeltica/MisskeyTools/blob/master/CHANGELOG.md';
/** Xeltica Studio 公式サイトURL */
export const XELTICA_STUDIO_URL = 'https://xeltica.work'; export const XELTICA_STUDIO_URL = 'https://xeltica.work';
/** ブレークポイント モバイル */
export const BREAKPOINT_SM = '800px'; export const BREAKPOINT_SM = '800px';

View file

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
import { IAnnouncement } from '../../common/types/announcement';
import { $get } from '../misc/api';
export const useAnnouncements = () => {
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
const fetchAllAnnouncements = () => {
setAnnouncements([]);
$get<IAnnouncement[]>('announcements').then(announcements => {
setAnnouncements(announcements ?? []);
});
};
useEffect(() => {
fetchAllAnnouncements();
}, []);
return announcements;
};

View file

@ -50,6 +50,8 @@
"shareMisskeyTools": "#MisskeyTools をシェアする", "shareMisskeyTools": "#MisskeyTools をシェアする",
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools", "shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
"instanceUrlPlaceholder": "例misskey.io", "instanceUrlPlaceholder": "例misskey.io",
"settings": "設定",
"accentColor": "アクセントカラー",
"_sidebar": { "_sidebar": {
"dashboard": "ダッシュボード", "dashboard": "ダッシュボード",
"tools": "ツール", "tools": "ツール",
@ -100,7 +102,6 @@
"useRanking": "ランキングに参加する" "useRanking": "ランキングに参加する"
}, },
"_accounts": { "_accounts": {
"currentAccount": "現在ログインしているアカウント",
"switchAccount": "アカウント切り替え", "switchAccount": "アカウント切り替え",
"useAnother": "他のアカウントで登録する" "useAnother": "他のアカウントで登録する"
}, },

View file

@ -16,7 +16,7 @@ export type Theme = typeof themes[number];
export type ActualTheme = typeof actualThemes[number]; export type ActualTheme = typeof actualThemes[number];
export const useTheme = () => { export const useTheme = () => {
const theme = useSelector(state => state.screen.theme); const {theme, accentColor} = useSelector(state => state.screen);
const [ osTheme, setOsTheme ] = useState<ActualTheme>('dark'); const [ osTheme, setOsTheme ] = useState<ActualTheme>('dark');
@ -45,4 +45,13 @@ export const useTheme = () => {
q.removeEventListener('change', listener); q.removeEventListener('change', listener);
}; };
}, [osTheme, setOsTheme]); }, [osTheme, setOsTheme]);
// カラー変更に追従する
useEffect(() => {
const {style} = document.body;
style.setProperty('--primary', `var(--${accentColor})`);
for (let i = 1; i <= 10; i++) {
style.setProperty(`--primary-${i}`, `var(--${accentColor}-${i})`);
}
}, [accentColor]);
}; };

View file

@ -33,12 +33,11 @@ export const AccountsPage: React.VFC = () => {
<Skeleton /> <Skeleton />
</div> </div>
) : ( ) : (
<article className="card fade"> <article className="fade">
<div className="body"> <section>
<div> <h2>{t('_accounts.switchAccount')}</h2>
<strong>{t('_accounts.switchAccount')}</strong>
</div> <div className="menu xmenu large fluid mb-2">
<div className="menu large fluid mb-2">
{ {
accounts.length === accountTokens.length ? ( accounts.length === accountTokens.length ? (
accounts.map(account => ( accounts.map(account => (
@ -59,8 +58,11 @@ export const AccountsPage: React.VFC = () => {
) )
} }
</div> </div>
</section>
<section>
<h2>{t('_accounts.useAnother')}</h2>
<LoginForm /> <LoginForm />
</div> </section>
</article> </article>
); );
}; };

View file

@ -21,10 +21,8 @@ export const AnnouncementPage: React.VFC = () => {
useEffect(() => { useEffect(() => {
$get<IAnnouncement>('announcements/' + id).then(setAnnouncement); $get<IAnnouncement>('announcements/' + id).then(setAnnouncement);
}, [setAnnouncement]); }, [setAnnouncement]);
return ( return !announcement ? <Skeleton width="100%" height="10rem" /> : (
<article> <article className="fade">
{!announcement ? <Skeleton width="100%" height="10rem" /> : (
<>
<h2> <h2>
{announcement.title} {announcement.title}
<aside className="inline ml-1 text-dimmed text-100"> <aside className="inline ml-1 text-dimmed text-100">
@ -35,8 +33,6 @@ export const AnnouncementPage: React.VFC = () => {
<section> <section>
<ReactMarkdown>{announcement.body}</ReactMarkdown> <ReactMarkdown>{announcement.body}</ReactMarkdown>
</section> </section>
</>
)}
</article> </article>
); );
}; };

View file

@ -1,5 +1,5 @@
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'; import React, { useCallback, useEffect, useReducer, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { alertModes } from '../../../common/types/alert-mode'; import { alertModes } from '../../../common/types/alert-mode';
@ -44,7 +44,6 @@ type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
export const MisshaiPage: React.VFC = () => { export const MisshaiPage: React.VFC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const session = useGetSessionQuery(undefined); const session = useGetSessionQuery(undefined);
const [limit, setLimit] = useState<number | undefined>(10);
const data = session.data; const data = session.data;
const score = useGetScoreQuery(undefined); const score = useGetScoreQuery(undefined);
@ -196,10 +195,9 @@ export const MisshaiPage: React.VFC = () => {
<Skeleton width="100%" height="160px" /> <Skeleton width="100%" height="160px" />
</div> </div>
) : ( ) : (
<div className="vstack fade"> <article className="fade">
<div className="card misshaiData"> <section className="misshaiData">
<div className="body"> <h2><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h2>
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
<table className="table fluid"> <table className="table fluid">
<thead> <thead>
<tr> <tr>
@ -226,31 +224,22 @@ export const MisshaiPage: React.VFC = () => {
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p> <p><strong>{t('_missHai.rating')}{': '}</strong>{session.data.rating}</p>
<strong> </section>
{t('_missHai.rating')}{': '} <section className="misshaiRanking">
</strong> <h2><i className="fas fa-ranking-star"/> {t('_missHai.ranking')}</h2>
{session.data.rating}
</p>
</div>
</div>
<div className="card misshaiRanking">
<div className="body">
<h1><i className="bi bi-bar-chart"></i> {t('_missHai.ranking')}</h1>
<Ranking limit={10} /> <Ranking limit={10} />
<Link to="/apps/miss-hai/ranking" className="btn primary" onClick={() => setLimit(undefined)}>{t('_missHai.showAll')}</Link> <Link to="/apps/miss-hai/ranking" className="block mt-2">{t('_missHai.showAll')}</Link>
</section>
<section className="alertModeSetting">
<h2><i className="fas fa-gear"></i> {t('settings')}</h2>
<label className="input-check mt-2"> <label className="input-check mt-2">
<input type="checkbox" checked={draft.useRanking} onChange={(e) => { <input type="checkbox" checked={draft.useRanking} onChange={(e) => {
updateSetting({ useRanking: e.target.checked }); updateSetting({ useRanking: e.target.checked });
}}/> }}/>
<span>{t('_missHai.useRanking')}</span> <span>{t('_missHai.useRanking')}</span>
</label> </label>
</div> <h3>{t('alertMode')}</h3>
</div>
<div className="misshaiPageLayout">
<div className="card alertModeSetting">
<div className="body">
<h1><i className="fas fa-gear"></i> {t('alertMode')}</h1>
<div className="vstack slim"> <div className="vstack slim">
{ alertModes.map((mode) => ( { alertModes.map((mode) => (
<label key={mode} className="input-check"> <label key={mode} className="input-check">
@ -269,7 +258,7 @@ export const MisshaiPage: React.VFC = () => {
)} )}
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && ( { (draft.alertMode === 'note' || draft.alertMode === 'both') && (
<> <>
<h2 className="mt-2 mb-1">{t('visibility')}</h2> <h3>{t('visibility')}</h3>
<div className="vstack slim"> <div className="vstack slim">
{ {
availableVisibilities.map((visibility) => ( availableVisibilities.map((visibility) => (
@ -290,11 +279,7 @@ export const MisshaiPage: React.VFC = () => {
</label> </label>
</> </>
)} )}
</div> <h3>{t('template')}</h3>
</div>
<div className="card templateSetting">
<div className="body">
<h1><i className="fas fa-pen-to-square"></i> {t('template')}</h1>
<p>{t('_template.description')}</p> <p>{t('_template.description')}</p>
<div className="hstack dense mb-2"> <div className="hstack dense mb-2">
<button className="btn" onClick={onClickInsertVariables}> <button className="btn" onClick={onClickInsertVariables}>
@ -309,16 +294,14 @@ export const MisshaiPage: React.VFC = () => {
dispatchDraft({ template: e.target.value }); dispatchDraft({ template: e.target.value });
}} /> }} />
<small className="text-dimmed">{t('_template.description2')}</small> <small className="text-dimmed">{t('_template.description2')}</small>
<div className="hstack mt-2" style={{justifyContent: 'flex-end'}}> <div className="hstack mt-2">
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button> <button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button>
<button className="btn primary" onClick={() => { <button className="btn primary" onClick={() => {
updateSettingWithDialog({ template: draft.template === '' ? null : draft.template }); updateSettingWithDialog({ template: draft.template === '' ? null : draft.template });
}}>{t('save')}</button> }}>{t('save')}</button>
</div> </div>
</div> </section>
</div> <section className="list-form mt-2">
</div>
<div className="list-form mt-2">
<button className="item" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}> <button className="item" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}>
<i className="icon fas fa-paper-plane" /> <i className="icon fas fa-paper-plane" />
<div className="body"> <div className="body">
@ -326,8 +309,8 @@ export const MisshaiPage: React.VFC = () => {
<p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p> <p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
</div> </div>
</button> </button>
</div> </section>
</div> </article>
); );
}; };

View file

@ -9,7 +9,7 @@ export const RankingPage: React.VFC = () => {
useTitle('_missHai.ranking'); useTitle('_missHai.ranking');
return ( return (
<article className="xarticle"> <article>
<h2>{t('_missHai.ranking')}</h2> <h2>{t('_missHai.ranking')}</h2>
<section> <section>
<p>{t('_missHai.rankingDescription')}</p> <p>{t('_missHai.rankingDescription')}</p>

View file

@ -7,8 +7,8 @@ import { IUser } from '../../common/types/user';
import { setAccounts } from '../store/slices/screen'; import { setAccounts } from '../store/slices/screen';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session'; import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { $get } from '../misc/api'; import { $get } from '../misc/api';
import { AnnouncementList } from '../components/AnnouncementList'; import { useAnnouncements } from '../hooks/useAnnouncements';
import { DeveloperInfo } from '../components/DeveloperInfo'; import { Link } from 'react-router-dom';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const {t} = useTranslation(); const {t} = useTranslation();
@ -16,22 +16,28 @@ export const IndexSessionPage: React.VFC = () => {
const { data: session } = useGetSessionQuery(undefined); const { data: session } = useGetSessionQuery(undefined);
const score = useGetScoreQuery(undefined); const score = useGetScoreQuery(undefined);
const announcements = useAnnouncements();
useEffect(() => { useEffect(() => {
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[]; const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[]))); Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
}, [dispatch]); }, [dispatch]);
return ( return (
<div className="vstack fade"> <article className="fade">
<div className="card announcement"> <section>
<div className="body"> <h2><i className="fas fa-bell"></i> {t('announcements')}</h2>
<AnnouncementList /> <div className="large menu xmenu fade">
</div> {announcements.map(a => (
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
{a.title}
</Link>
))}
</div> </div>
</section>
<div className="misshaiPageLayout"> <div className="misshaiPageLayout">
<div className="card misshaiData"> <section className="misshaiData">
<div className="body"> <h2><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h2>
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
<table className="table fluid"> <table className="table fluid">
<thead> <thead>
<tr> <tr>
@ -64,14 +70,26 @@ export const IndexSessionPage: React.VFC = () => {
</strong> </strong>
{session?.rating ?? '...'} {session?.rating ?? '...'}
</p> </p>
</section>
<section className="developerInfo">
<h2><i className="fas fa-circle-question"></i> {t('_developerInfo.title')}</h2>
<p>{t('_developerInfo.description')}</p>
<div className="menu large">
<a className="item" href="http://groundpolis.app/@Lutica" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
Lutica@groundpolis.app
</a>
<a className="item" href="http://misskey.io/@le" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
le@misskey.io
</a>
<a className="item" href="http://twitter.com/@EbiseLutica" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
EbiseLutica@twitter.com
</a>
</div> </div>
</section>
</div> </div>
<div className="card developerInfo"> </article>
<div className="body">
<DeveloperInfo />
</div>
</div>
</div>
</div>
); );
}; };

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -6,9 +6,8 @@ import { LoginForm } from '../components/LoginForm';
import styled from 'styled-components'; import styled from 'styled-components';
import { useSelector } from '../store'; import { useSelector } from '../store';
import { IsMobileProp } from '../misc/is-mobile-prop'; import { IsMobileProp } from '../misc/is-mobile-prop';
import { IAnnouncement } from '../../common/types/announcement';
import { $get } from '../misc/api';
import Twemoji from 'react-twemoji'; import Twemoji from 'react-twemoji';
import { useAnnouncements } from '../hooks/useAnnouncements';
const Hero = styled.div<IsMobileProp>` const Hero = styled.div<IsMobileProp>`
display: flex; display: flex;
@ -68,21 +67,10 @@ const FormWrapper = styled.div`
`; `;
export const IndexWelcomePage: React.VFC = () => { export const IndexWelcomePage: React.VFC = () => {
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
const {isMobile} = useSelector(state => state.screen); const {isMobile} = useSelector(state => state.screen);
const {t} = useTranslation(); const {t} = useTranslation();
const fetchAllAnnouncements = () => { const announcements = useAnnouncements();
setAnnouncements([]);
$get<IAnnouncement[]>('announcements').then(announcements => {
setAnnouncements(announcements ?? []);
});
};
useEffect(() => {
fetchAllAnnouncements();
}, []);
return ( return (
<> <>
@ -96,8 +84,8 @@ export const IndexWelcomePage: React.VFC = () => {
</FormWrapper> </FormWrapper>
</div> </div>
<div className="announcements"> <div className="announcements">
<h2></h2> <h2><i className="fas fa-bell"></i> {t('announcements')}</h2>
<div className="menu large"> <div className="menu xmenu">
{announcements.map(a => ( {announcements.map(a => (
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}> <Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
{a.title} {a.title}

View file

@ -6,11 +6,33 @@ import { useGetSessionQuery } from '../services/session';
import { Card } from '../components/Card'; import { Card } from '../components/Card';
import { Theme, themes } from '../misc/theme'; import { Theme, themes } from '../misc/theme';
import { LOCALSTORAGE_KEY_TOKEN } from '../const'; import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { changeLang, changeTheme, showModal } from '../store/slices/screen'; import { changeAccentColor, changeLang, changeTheme, showModal } from '../store/slices/screen';
import { useSelector } from '../store'; import { useSelector } from '../store';
import { languageName } from '../langs'; import { languageName } from '../langs';
import { $delete } from '../misc/api'; import { $delete } from '../misc/api';
import { useTitle } from '../hooks/useTitle'; import { useTitle } from '../hooks/useTitle';
import { designSystemColors } from '../../common/types/design-system-color';
import styled from 'styled-components';
const ColorInput = styled.input<{color: string}>`
display: block;
appearance: none;
width: 32px;
height: 32px;
border-radius: 999px;
background-color: var(--panel);
border: 4px solid var(--${p => p.color});
cursor: pointer;
transition: all 0.2s ease;
&:checked {
background: var(--${p => p.color});
cursor: default;
}
&:hover, &:focus {
box-shadow: 0 0 16px var(--${p => p.color});
outline: none;
}
`;
export const SettingPage: React.VFC = () => { export const SettingPage: React.VFC = () => {
const session = useGetSessionQuery(undefined); const session = useGetSessionQuery(undefined);
@ -23,6 +45,7 @@ export const SettingPage: React.VFC = () => {
const currentTheme = useSelector(state => state.screen.theme); const currentTheme = useSelector(state => state.screen.theme);
const currentLang = useSelector(state => state.screen.language); const currentLang = useSelector(state => state.screen.language);
const currentAccentColor = useSelector(state => state.screen.accentColor);
const onClickLogout = useCallback(() => { const onClickLogout = useCallback(() => {
dispatch(showModal({ dispatch(showModal({
@ -92,10 +115,10 @@ export const SettingPage: React.VFC = () => {
return session.isLoading || !data ? ( return session.isLoading || !data ? (
<div className="skeleton" style={{width: '100%', height: '128px'}}></div> <div className="skeleton" style={{width: '100%', height: '128px'}}></div>
) : ( ) : (
<div className="vstack fade"> <article className="fade">
<Card bodyClassName="vstack"> <h2><i className="fas fa-palette"></i> {t('appearance')}</h2>
<h1><i className="fas fa-palette"></i> {t('appearance')}</h1> <section>
<h2>{t('theme')}</h2> <h3>{t('theme')}</h3>
<div className="vstack"> <div className="vstack">
{ {
themes.map(theme => ( themes.map(theme => (
@ -106,11 +129,19 @@ export const SettingPage: React.VFC = () => {
)) ))
} }
</div> </div>
</section>
<h2>{t('language')}</h2> <section>
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => { <h3>{t('accentColor')}</h3>
dispatch(changeLang(e.target.value)); <div className="hstack slim wrap mb-2">
}}> {designSystemColors.map(c => (
<ColorInput className="shadow-2" type="radio" color={c} value={c} checked={c === currentAccentColor} onChange={e => dispatch(changeAccentColor(e.target.value))} />
))}
</div>
<button className="btn primary">{t('resetToDefault')}</button>
</section>
<section>
<h3>{t('language')}</h3>
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => dispatch(changeLang(e.target.value))}>
{ {
(Object.keys(languageName) as Array<keyof typeof languageName>).map(n => ( (Object.keys(languageName) as Array<keyof typeof languageName>).map(n => (
<option value={n} key={n}>{languageName[n]}</option> <option value={n} key={n}>{languageName[n]}</option>
@ -124,7 +155,9 @@ export const SettingPage: React.VFC = () => {
<a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a> <a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a>
</div> </div>
</div> </div>
</Card> </section>
<section>
<h2></h2>
<div className="list-form"> <div className="list-form">
<button className="item" onClick={onClickLogout}> <button className="item" onClick={onClickLogout}>
<i className="icon fas fa-arrow-up-right-from-square" /> <i className="icon fas fa-arrow-up-right-from-square" />
@ -141,6 +174,7 @@ export const SettingPage: React.VFC = () => {
</div> </div>
</button> </button>
</div> </div>
</div> </section>
</article>
); );
}; };

View file

@ -1,11 +1,12 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import i18n from 'i18next'; import i18n from 'i18next';
import { WritableDraft } from 'immer/dist/internal'; import { WritableDraft } from 'immer/dist/internal';
import { IUser } from '../../../common/types/user';
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const'; import { LOCALSTORAGE_KEY_ACCENT_COLOR, 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';
import { IUser } from '../../../common/types/user';
import { DesignSystemColor } from '../../../common/types/design-system-color';
interface ScreenState { interface ScreenState {
modal: Modal | null; modal: Modal | null;
@ -13,6 +14,7 @@ interface ScreenState {
theme: Theme; theme: Theme;
title: string | null; title: string | null;
language: string; language: string;
accentColor: DesignSystemColor;
accounts: IUser[]; accounts: IUser[];
accountTokens: string[]; accountTokens: string[];
isMobile: boolean; isMobile: boolean;
@ -24,6 +26,7 @@ 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',
accentColor: localStorage[LOCALSTORAGE_KEY_ACCENT_COLOR] ?? 'green',
title: null, title: null,
accounts: [], accounts: [],
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[], accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
@ -60,6 +63,9 @@ export const screenSlice = createSlice({
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload; localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
i18n.changeLanguage(action.payload); i18n.changeLanguage(action.payload);
}), }),
changeAccentColor: generateSetter('accentColor', (_, action) => {
localStorage[LOCALSTORAGE_KEY_ACCENT_COLOR] = action.payload;
}),
setAccounts: generateSetter('accounts', (state, action) => { setAccounts: generateSetter('accounts', (state, action) => {
state.accountTokens = action.payload.map(a => a.misshaiToken); state.accountTokens = action.payload.map(a => a.misshaiToken);
localStorage[LOCALSTORAGE_KEY_ACCOUNTS] = JSON.stringify(state.accountTokens); localStorage[LOCALSTORAGE_KEY_ACCOUNTS] = JSON.stringify(state.accountTokens);
@ -70,6 +76,6 @@ export const screenSlice = createSlice({
}, },
}); });
export const { showModal, hideModal, changeTheme, changeLang, setAccounts, setMobile, setTitle, setDrawerShown } = screenSlice.actions; export const { showModal, hideModal, changeTheme, changeLang, changeAccentColor, setAccounts, setMobile, setTitle, setDrawerShown } = screenSlice.actions;
export default screenSlice.reducer; export default screenSlice.reducer;

View file

@ -1,6 +1,4 @@
body { body {
--primary: rgb(134, 179, 0);
--primary-d: rgb(52, 70, 0);
--max-width: 1024px; --max-width: 1024px;
font-family: "Koruri", sans-serif; font-family: "Koruri", sans-serif;
} }
@ -19,6 +17,19 @@ hr {
max-width: var(--max-width); max-width: var(--max-width);
} }
.xmenu {
.item {
background: #ffffff40;
+ .item {
margin-top: var(--slim-margin);
}
}
}
.dark .xmenu .item {
background: #00000040;
}
._header { ._header {
position: sticky; position: sticky;
top: var(--container-padding); top: var(--container-padding);