mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 05:54:19 +01:00
feat: enable to save/restore configuration
This commit is contained in:
@@ -4,13 +4,14 @@ import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
|||||||
|
|
||||||
import TimelineContentDisplay from '@/components/timeline/TimelineContentDisplay';
|
import TimelineContentDisplay from '@/components/timeline/TimelineContentDisplay';
|
||||||
import { TimelineContext, useTimelineState } from '@/components/timeline/TimelineContext';
|
import { TimelineContext, useTimelineState } from '@/components/timeline/TimelineContext';
|
||||||
|
import { ColumnWidth } from '@/core/column';
|
||||||
import { useHandleCommand } from '@/hooks/useCommandBus';
|
import { useHandleCommand } from '@/hooks/useCommandBus';
|
||||||
import { useTranslation } from '@/i18n/useTranslation';
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
|
|
||||||
export type ColumnProps = {
|
export type ColumnProps = {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
lastColumn: boolean;
|
lastColumn: boolean;
|
||||||
width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined;
|
width: ColumnWidth;
|
||||||
header: JSX.Element;
|
header: JSX.Element;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import UserNameDisplay from '@/components/UserDisplayName';
|
|||||||
import LazyLoad from '@/components/utils/LazyLoad';
|
import LazyLoad from '@/components/utils/LazyLoad';
|
||||||
import usePopup from '@/components/utils/usePopup';
|
import usePopup from '@/components/utils/usePopup';
|
||||||
import { colorThemes } from '@/core/colorThemes';
|
import { colorThemes } from '@/core/colorThemes';
|
||||||
import useConfig, { type Config } from '@/core/useConfig';
|
import useConfig, { ConfigSchema, type Config } from '@/core/useConfig';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
import { useTranslation } from '@/i18n/useTranslation';
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import usePubkey from '@/nostr/usePubkey';
|
import usePubkey from '@/nostr/usePubkey';
|
||||||
@@ -83,7 +83,7 @@ const ProfileSection = () => {
|
|||||||
const { showProfile, showProfileEdit } = useModalState();
|
const { showProfile, showProfileEdit } = useModalState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title={i18n.t('config.profile.profile')}>
|
<Section title={i18n.t('config.account.profile')}>
|
||||||
<div class="flex gap-2 py-1">
|
<div class="flex gap-2 py-1">
|
||||||
<button
|
<button
|
||||||
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
||||||
@@ -93,19 +93,98 @@ const ProfileSection = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{i18n.t('config.profile.openProfile')}
|
{i18n.t('config.account.openProfile')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
||||||
onClick={() => showProfileEdit()}
|
onClick={() => showProfileEdit()}
|
||||||
>
|
>
|
||||||
{i18n.t('config.profile.editProfile')}
|
{i18n.t('config.account.editProfile')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BackupSection = () => {
|
||||||
|
let fileInputRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
const i18n = useTranslation();
|
||||||
|
const config = useConfig();
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const json = JSON.stringify(config.config(), null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const dataUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const datetime = new Date().toISOString();
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = `rabbit-${datetime}.json`;
|
||||||
|
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestore = () => {
|
||||||
|
if (fileInputRef == null) return;
|
||||||
|
fileInputRef.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const restore = async (file: File) => {
|
||||||
|
try {
|
||||||
|
const json = await file.text();
|
||||||
|
const validated = ConfigSchema.parse(JSON.parse(json));
|
||||||
|
config.setConfig(validated);
|
||||||
|
|
||||||
|
window.alert(i18n.t('config.account.restored'));
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
window.alert(`${i18n.t('config.account.failedToRestore')}: ${e.message}`);
|
||||||
|
} else {
|
||||||
|
window.alert(i18n.t('config.account.failedToRestore'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeFile: JSX.EventHandler<HTMLInputElement, Event> = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const files = [...(ev.currentTarget.files ?? [])];
|
||||||
|
if (files.length !== 1) return;
|
||||||
|
const file = files[0];
|
||||||
|
restore(file).catch((err) => console.log(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title={i18n.t('config.account.backupConfig')}>
|
||||||
|
<div class="flex gap-2 py-1">
|
||||||
|
<button
|
||||||
|
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{i18n.t('config.account.save')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
||||||
|
onClick={handleRestore}
|
||||||
|
>
|
||||||
|
{i18n.t('config.account.restore')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
hidden
|
||||||
|
multiple={false}
|
||||||
|
name="config"
|
||||||
|
accept="application/json"
|
||||||
|
onChange={handleChangeFile}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const RelayConfig = () => {
|
const RelayConfig = () => {
|
||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const { config, addRelay, removeRelay } = useConfig();
|
const { config, addRelay, removeRelay } = useConfig();
|
||||||
@@ -639,9 +718,14 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
|
|
||||||
const menu = [
|
const menu = [
|
||||||
{
|
{
|
||||||
name: () => i18n.t('config.profile.profile'),
|
name: () => i18n.t('config.account.profile'),
|
||||||
icon: () => <User />,
|
icon: () => <User />,
|
||||||
render: () => <ProfileSection />,
|
render: () => (
|
||||||
|
<>
|
||||||
|
<ProfileSection />
|
||||||
|
<BackupSection />
|
||||||
|
</>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: () => i18n.t('config.relays.relays'),
|
name: () => i18n.t('config.relays.relays'),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { type Filter } from 'nostr-tools/filter';
|
|
||||||
|
|
||||||
import { type ColumnProps } from '@/components/column/Column';
|
import { ContentFilterSchema } from '@/core/contentFilter';
|
||||||
import { ContentFilter } from '@/core/contentFilter';
|
|
||||||
import { relaysForJapaneseTL } from '@/core/relayUrls';
|
import { relaysForJapaneseTL } from '@/core/relayUrls';
|
||||||
import generateId from '@/utils/generateId';
|
import generateId from '@/utils/generateId';
|
||||||
|
|
||||||
@@ -41,80 +39,100 @@ export type NotificationFilterOptions = {
|
|||||||
// */
|
// */
|
||||||
// export const buildFilter = (options: BuildOptions) => {};
|
// export const buildFilter = (options: BuildOptions) => {};
|
||||||
|
|
||||||
export type BaseColumn = {
|
export const ColumnWidthSchema = z.union([
|
||||||
id: string;
|
z.literal('widest'),
|
||||||
name?: string;
|
z.literal('wide'),
|
||||||
width: ColumnProps['width'];
|
z.literal('medium'),
|
||||||
contentFilter?: ContentFilter;
|
z.literal('narrow'),
|
||||||
};
|
]);
|
||||||
|
|
||||||
|
export type ColumnWidth = z.infer<typeof ColumnWidthSchema>;
|
||||||
|
|
||||||
|
export const BaseColumnSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
width: ColumnWidthSchema,
|
||||||
|
contentFilter: z.optional(ContentFilterSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BaseColumn = z.infer<typeof BaseColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows posts by following users */
|
/** A column which shows posts by following users */
|
||||||
export type FollowingColumnType = BaseColumn & {
|
export const FollowingColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Following';
|
columnType: z.literal('Following'),
|
||||||
pubkey: string;
|
pubkey: z.string(),
|
||||||
};
|
});
|
||||||
|
export type FollowingColumnType = z.infer<typeof FollowingColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows replies, reactions, reposts to the specific user */
|
/** A column which shows replies, reactions, reposts to the specific user */
|
||||||
export type NotificationColumnType = BaseColumn & {
|
export const NotificationColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Notification';
|
columnType: z.literal('Notification'),
|
||||||
// notificationTypes: NotificationType[];
|
pubkey: z.string(),
|
||||||
pubkey: string;
|
// notificationTypes: z.array(NotificationTypeSchema),
|
||||||
};
|
});
|
||||||
|
export type NotificationColumnType = z.infer<typeof NotificationColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows posts from the specific user */
|
/** A column which shows posts from the specific user */
|
||||||
export type PostsColumnType = BaseColumn & {
|
export const PostsColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Posts';
|
columnType: z.literal('Posts'),
|
||||||
pubkey: string;
|
pubkey: z.string(),
|
||||||
};
|
});
|
||||||
|
export type PostsColumnType = z.infer<typeof PostsColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows reactions published by the specific user */
|
/** A column which shows reactions published by the specific user */
|
||||||
export type ReactionsColumnType = BaseColumn & {
|
export const ReactionsColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Reactions';
|
columnType: z.literal('Reactions'),
|
||||||
pubkey: string;
|
pubkey: z.string(),
|
||||||
};
|
});
|
||||||
|
export type ReactionsColumnType = z.infer<typeof ReactionsColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows reactions published by the specific user */
|
/** A column which shows posts published on the specific channel */
|
||||||
export type ChannelColumnType = BaseColumn & {
|
export const ChannelColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Channel';
|
columnType: z.literal('Channel'),
|
||||||
rootEventId: string;
|
rootEventId: z.string(),
|
||||||
};
|
});
|
||||||
|
export type ChannelColumnType = z.infer<typeof ChannelColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows text notes and reposts posted to the specific relays */
|
/** A column which shows text notes and reposts posted to the specific relays */
|
||||||
export type RelaysColumnType = BaseColumn & {
|
export const RelaysColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Relays';
|
columnType: z.literal('Relays'),
|
||||||
relayUrls: string[];
|
relayUrls: z.array(z.string()),
|
||||||
};
|
});
|
||||||
|
export type RelaysColumnType = z.infer<typeof RelaysColumnSchema>;
|
||||||
|
|
||||||
/** A column which search text notes from relays which support NIP-50 */
|
/** A column which search text notes from relays which support NIP-50 */
|
||||||
export type SearchColumnType = BaseColumn & {
|
export const SearchColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Search';
|
columnType: z.literal('Search'),
|
||||||
query: string;
|
query: z.string(),
|
||||||
};
|
});
|
||||||
|
export type SearchColumnType = z.infer<typeof SearchColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows events in the bookmark */
|
/** A column which shows events in the bookmark */
|
||||||
export type BookmarkColumnType = BaseColumn & {
|
export const BookmarkColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'Bookmark';
|
columnType: z.literal('Bookmark'),
|
||||||
pubkey: string;
|
pubkey: z.string(),
|
||||||
identifier: string;
|
identifier: z.string(),
|
||||||
};
|
});
|
||||||
|
export type BookmarkColumnType = z.infer<typeof BookmarkColumnSchema>;
|
||||||
|
|
||||||
/** A column which shows text notes and reposts posted to the specific relays */
|
// /** A column which shows text notes and reposts posted to the specific relays */
|
||||||
export type CustomFilterColumnType = BaseColumn & {
|
// const CustomFilterColumnSchema = BaseColumnSchema.extend({
|
||||||
columnType: 'CustomFilter';
|
// columnType: z.literal('CustomFilter'),
|
||||||
filters: Filter[];
|
// filters: z.array(z.any()), // Replace `z.any()` with the appropriate Zod schema if `Filter` is known
|
||||||
};
|
// });
|
||||||
|
|
||||||
export type ColumnType =
|
export const ColumnTypeSchema = z.union([
|
||||||
| FollowingColumnType
|
FollowingColumnSchema,
|
||||||
| NotificationColumnType
|
NotificationColumnSchema,
|
||||||
| PostsColumnType
|
PostsColumnSchema,
|
||||||
| ReactionsColumnType
|
ReactionsColumnSchema,
|
||||||
| ChannelColumnType
|
ChannelColumnSchema,
|
||||||
| RelaysColumnType
|
RelaysColumnSchema,
|
||||||
| SearchColumnType
|
SearchColumnSchema,
|
||||||
| BookmarkColumnType;
|
BookmarkColumnSchema,
|
||||||
/* WIP: */
|
]);
|
||||||
/* | CustomFilterColumnType */
|
|
||||||
|
export type ColumnType = z.infer<typeof ColumnTypeSchema>;
|
||||||
|
|
||||||
type CreateParams<T extends BaseColumn> = Omit<T, keyof BaseColumn | 'columnType'> &
|
type CreateParams<T extends BaseColumn> = Omit<T, keyof BaseColumn | 'columnType'> &
|
||||||
Partial<BaseColumn>;
|
Partial<BaseColumn>;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content filter is a text which expresses the filter.
|
* Content filter is a text which expresses the filter.
|
||||||
*
|
*
|
||||||
@@ -19,6 +21,7 @@
|
|||||||
* Case-sensitive
|
* Case-sensitive
|
||||||
* "Hello World"
|
* "Hello World"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ContentFilterAnd = { filterType: 'AND'; children: ContentFilter[] };
|
export type ContentFilterAnd = { filterType: 'AND'; children: ContentFilter[] };
|
||||||
export type ContentFilterOr = { filterType: 'OR'; children: ContentFilter[] };
|
export type ContentFilterOr = { filterType: 'OR'; children: ContentFilter[] };
|
||||||
export type ContentFilterNot = { filterType: 'NOT'; child: ContentFilter };
|
export type ContentFilterNot = { filterType: 'NOT'; child: ContentFilter };
|
||||||
@@ -32,6 +35,55 @@ export type ContentFilter =
|
|||||||
| ContentFilterTextInclude
|
| ContentFilterTextInclude
|
||||||
| ContentFilterRegex;
|
| ContentFilterRegex;
|
||||||
|
|
||||||
|
export const ContentFilterAndSchema = z.object({
|
||||||
|
filterType: z.literal('AND'),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
|
children: z.array(z.lazy(() => ContentFilterSchema)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContentFilterOrSchema = z.object({
|
||||||
|
filterType: z.literal('OR'),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
|
children: z.array(z.lazy(() => ContentFilterSchema)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContentFilterNotSchema = z.object({
|
||||||
|
filterType: z.literal('NOT'),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
|
child: z.lazy(() => ContentFilterSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContentFilterTextIncludeSchema = z.object({
|
||||||
|
filterType: z.literal('Text'),
|
||||||
|
text: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContentFilterRegexSchema = z
|
||||||
|
.object({
|
||||||
|
filterType: z.literal('Regex'),
|
||||||
|
regex: z.string(),
|
||||||
|
flag: z.string(),
|
||||||
|
})
|
||||||
|
.refine(({ regex, flag }) => {
|
||||||
|
try {
|
||||||
|
/* eslint-disable-next-line no-new */
|
||||||
|
new RegExp(regex, flag);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 再帰的型定義を用いる場合、Zodでは型情報を提供しなければならない。
|
||||||
|
// https://github.com/colinhacks/zod#recursive-types
|
||||||
|
export const ContentFilterSchema: z.ZodSchema<ContentFilter> = z.union([
|
||||||
|
ContentFilterAndSchema,
|
||||||
|
ContentFilterOrSchema,
|
||||||
|
ContentFilterNotSchema,
|
||||||
|
ContentFilterTextIncludeSchema,
|
||||||
|
ContentFilterRegexSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
export const applyContentFilter =
|
export const applyContentFilter =
|
||||||
(filter: ContentFilter) =>
|
(filter: ContentFilter) =>
|
||||||
(content: string): boolean => {
|
(content: string): boolean => {
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import sortBy from 'lodash/sortBy';
|
|||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import * as Kind from 'nostr-tools/kinds';
|
import * as Kind from 'nostr-tools/kinds';
|
||||||
import { type Event as NostrEvent } from 'nostr-tools/pure';
|
import { type Event as NostrEvent } from 'nostr-tools/pure';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { colorThemes, type ColorTheme } from '@/core/colorThemes';
|
import { colorThemes, type ColorTheme } from '@/core/colorThemes';
|
||||||
import {
|
import {
|
||||||
ColumnType,
|
ColumnType,
|
||||||
|
ColumnTypeSchema,
|
||||||
createFollowingColumn,
|
createFollowingColumn,
|
||||||
createJapanRelaysColumn,
|
createJapanRelaysColumn,
|
||||||
createNotificationColumn,
|
createNotificationColumn,
|
||||||
@@ -23,35 +25,45 @@ import { useTranslation } from '@/i18n/useTranslation';
|
|||||||
import { genericEvent } from '@/nostr/event';
|
import { genericEvent } from '@/nostr/event';
|
||||||
import { asCaseInsensitive, wordsRegex } from '@/utils/regex';
|
import { asCaseInsensitive, wordsRegex } from '@/utils/regex';
|
||||||
|
|
||||||
export type CustomEmojiConfig = {
|
const CustomEmojiConfigSchema = z.object({
|
||||||
shortcode: string;
|
shortcode: z.string(),
|
||||||
url: string;
|
url: z.string(),
|
||||||
};
|
});
|
||||||
|
|
||||||
export type ColorThemeConfig = {
|
export type CustomEmojiConfig = z.infer<typeof CustomEmojiConfigSchema>;
|
||||||
type: 'specific';
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Config = {
|
const ColorThemeConfigSchema = z.object({
|
||||||
relayUrls: string[];
|
type: z.literal('specific'),
|
||||||
columns: ColumnType[];
|
id: z.string(),
|
||||||
customEmojis: Record<string, CustomEmojiConfig>;
|
});
|
||||||
colorTheme: ColorThemeConfig;
|
|
||||||
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
export type ColorThemeConfig = z.infer<typeof ColorThemeConfigSchema>;
|
||||||
keepOpenPostForm: boolean;
|
|
||||||
useEmojiReaction: boolean;
|
export const ConfigSchema = z.object({
|
||||||
showEmojiReaction: boolean;
|
relayUrls: z.array(z.string()),
|
||||||
showMedia: boolean; // TODO 'always' | 'only-followings' | 'never'
|
columns: z.array(ColumnTypeSchema),
|
||||||
embedding: {
|
customEmojis: z.record(CustomEmojiConfigSchema),
|
||||||
twitter: boolean;
|
colorTheme: ColorThemeConfigSchema,
|
||||||
youtube: boolean;
|
dateFormat: z.union([
|
||||||
ogp: boolean;
|
z.literal('relative'),
|
||||||
};
|
z.literal('absolute-long'),
|
||||||
hideCount: boolean;
|
z.literal('absolute-short'),
|
||||||
mutedPubkeys: string[];
|
]),
|
||||||
mutedKeywords: string[];
|
keepOpenPostForm: z.boolean(),
|
||||||
};
|
useEmojiReaction: z.boolean(),
|
||||||
|
showEmojiReaction: z.boolean(),
|
||||||
|
showMedia: z.boolean(), // TODO 'always' | 'only-followings' | 'never'に変更
|
||||||
|
embedding: z.object({
|
||||||
|
twitter: z.boolean(),
|
||||||
|
youtube: z.boolean(),
|
||||||
|
ogp: z.boolean(),
|
||||||
|
}),
|
||||||
|
hideCount: z.boolean(),
|
||||||
|
mutedPubkeys: z.array(z.string()),
|
||||||
|
mutedKeywords: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
|
|
||||||
type UseConfig = {
|
type UseConfig = {
|
||||||
config: Accessor<Config>;
|
config: Accessor<Config>;
|
||||||
|
|||||||
@@ -159,10 +159,15 @@ export default {
|
|||||||
confirmImport: 'Import? (The config will be overwritten)',
|
confirmImport: 'Import? (The config will be overwritten)',
|
||||||
copyToClipboard: 'Copy to clipboard',
|
copyToClipboard: 'Copy to clipboard',
|
||||||
importFromClipboard: 'Import from clipboard',
|
importFromClipboard: 'Import from clipboard',
|
||||||
profile: {
|
account: {
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
openProfile: 'Open',
|
openProfile: 'Open',
|
||||||
editProfile: 'Edit',
|
editProfile: 'Edit',
|
||||||
|
backupConfig: 'Backup configuration',
|
||||||
|
save: 'Save',
|
||||||
|
restore: 'Restore',
|
||||||
|
restored: 'Successfully restored.',
|
||||||
|
failedToRestore: 'Failed to restore',
|
||||||
},
|
},
|
||||||
relays: {
|
relays: {
|
||||||
relays: 'Relays',
|
relays: 'Relays',
|
||||||
|
|||||||
@@ -155,10 +155,15 @@ export default {
|
|||||||
confirmImport: 'インポートしますか?(現在の設定は上書きされます)',
|
confirmImport: 'インポートしますか?(現在の設定は上書きされます)',
|
||||||
copyToClipboard: 'クリップボードに設定をコピー',
|
copyToClipboard: 'クリップボードに設定をコピー',
|
||||||
importFromClipboard: '設定をクリップボードから読み込む',
|
importFromClipboard: '設定をクリップボードから読み込む',
|
||||||
profile: {
|
account: {
|
||||||
profile: 'プロフィール',
|
profile: 'プロフィール',
|
||||||
openProfile: '開く',
|
openProfile: '開く',
|
||||||
editProfile: '編集',
|
editProfile: '編集',
|
||||||
|
backupConfig: '設定のバックアップ',
|
||||||
|
save: '保存',
|
||||||
|
restore: '復元',
|
||||||
|
restored: '復元しました',
|
||||||
|
failedToRestore: '復元に失敗しました',
|
||||||
},
|
},
|
||||||
relays: {
|
relays: {
|
||||||
relays: 'リレー',
|
relays: 'リレー',
|
||||||
|
|||||||
Reference in New Issue
Block a user