このコミットが含まれているのは:
2026-07-04 21:47:16 +09:00
コミット 518c5fa0f2
16個のファイルの変更840行の追加598行の削除
+92 -70
ファイルの表示
@@ -1,49 +1,38 @@
import { apiGet, apiPatch } from '@/lib/api'
import type { FetchPostsOrder } from '@/types'
// DB-backed user settings. These are shared across browsers for the same user.
export type UserPostListOrder =
| 'title_asc'
| 'title_desc'
| 'url_asc'
| 'url_desc'
| 'original_created_at_asc'
| 'original_created_at_desc'
| 'created_at_asc'
| 'created_at_desc'
| 'updated_at_asc'
| 'updated_at_desc'
import type { FetchPostsOrder, FetchTagsOrder } from '@/types'
// DB-backed user settings. These are worth sharing across browsers for one user.
export type UserSettings = {
theme: 'system' | 'light' | 'dark'
displayDensity: 'comfortable' | 'compact'
fontSize: 'small' | 'normal' | 'large'
postListLimit: 20 | 50 | 100
postListOrder: UserPostListOrder
viewedPostDisplay: 'show' | 'dim'
tagAutocompleteNico: boolean
autoFetchTitle: 'auto' | 'manual' | 'off'
autoFetchThumbnail: 'auto' | 'manual' | 'off'
wikiEditorMode: 'split' | 'write' | 'preview' }
theme: 'system' | 'light' | 'dark'
autoFetchTitle: 'auto' | 'manual' | 'off'
autoFetchThumbnail: 'auto' | 'manual' | 'off'
wikiEditorMode: 'split' | 'write' | 'preview' }
export type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom'
export type TheatreTagFlow = 'vertical' | 'horizontal'
export type GekanatorBackgroundMotionMode = 'on' | 'calm' | 'off'
export type ClientPaneBreakpoint = 'desktop' | 'tablet'
export type ClientListKey = 'postList' | 'postSearch' | 'tagList'
export type ClientListLimit = 20 | 50 | 100
// Browser-local settings. These stay in localStorage and are never synced to DB.
type ClientPaneSettings = {
widthPxByBreakpoint?: Partial<Record<ClientPaneBreakpoint, number>>
collapsed?: boolean }
type ClientListSettings = {
limit?: ClientListLimit
order?: string }
// Browser-local settings. These depend on device or screen context.
export type ClientSettings = {
panes?: Record<string, ClientPaneSettings>
theatre?: {
panes?: Record<string, ClientPaneSettings>
lists?: Partial<Record<ClientListKey, ClientListSettings>>
theatre?: {
layoutMode?: TheatreLayoutMode
tagFlow?: TheatreTagFlow
}
gekanator?: {
gekanator?: {
backgroundMotion?: GekanatorBackgroundMotionMode
}
reducedMotion?: string
@@ -52,36 +41,42 @@ export type ClientSettings = {
export const CLIENT_SETTINGS_STORAGE_KEY = 'btrc_hub.client_settings'
export const DEFAULT_USER_SETTINGS: UserSettings = {
theme: 'system',
displayDensity: 'comfortable',
fontSize: 'normal',
postListLimit: 50,
postListOrder: 'created_at_desc',
viewedPostDisplay: 'show',
tagAutocompleteNico: true,
autoFetchTitle: 'manual',
autoFetchThumbnail: 'manual',
wikiEditorMode: 'split' }
theme: 'system',
autoFetchTitle: 'manual',
autoFetchThumbnail: 'manual',
wikiEditorMode: 'split' }
export const THEME_OPTIONS = ['system', 'light', 'dark'] as const
export const DISPLAY_DENSITY_OPTIONS = ['comfortable', 'compact'] as const
export const FONT_SIZE_OPTIONS = ['small', 'normal', 'large'] as const
export const POST_LIST_LIMIT_OPTIONS = [20, 50, 100] as const
export const POST_LIST_ORDER_OPTIONS = [
'title_asc',
'title_desc',
'url_asc',
'url_desc',
'original_created_at_asc',
'original_created_at_desc',
'created_at_asc',
'created_at_desc',
'updated_at_asc',
'updated_at_desc',
] as const
export const VIEWED_POST_DISPLAY_OPTIONS = ['show', 'dim'] as const
export const AUTO_FETCH_OPTIONS = ['auto', 'manual', 'off'] as const
export const WIKI_EDITOR_MODE_OPTIONS = ['split', 'write', 'preview'] as const
export const LIST_LIMIT_OPTIONS = [20, 50, 100] as const
export const POST_LIST_ORDER_OPTIONS = [
'title:asc',
'title:desc',
'url:asc',
'url:desc',
'original_created_at:asc',
'original_created_at:desc',
'created_at:asc',
'created_at:desc',
'updated_at:asc',
'updated_at:desc',
] as const satisfies readonly FetchPostsOrder[]
export const TAG_LIST_ORDER_OPTIONS = [
'name:asc',
'name:desc',
'category:asc',
'category:desc',
'post_count:asc',
'post_count:desc',
'created_at:asc',
'created_at:desc',
'updated_at:asc',
'updated_at:desc',
] as const satisfies readonly FetchTagsOrder[]
export const DEFAULT_POST_LIST_LIMIT: ClientListLimit = 20
export const DEFAULT_POST_LIST_ORDER: FetchPostsOrder = 'original_created_at:desc'
export const DEFAULT_TAG_LIST_ORDER: FetchTagsOrder = 'name:asc'
const LEGACY_THEATRE_LAYOUT_STORAGE_KEY = 'theatre-layout-mode'
const LEGACY_THEATRE_TAG_FLOW_STORAGE_KEY = 'theatre-tag-flow'
@@ -95,26 +90,14 @@ export const fetchUserSettings = async (): Promise<UserSettings> =>
export const updateUserSettings = async (
settings: Partial<{
theme: UserSettings['theme']
display_density: UserSettings['displayDensity']
font_size: UserSettings['fontSize']
post_list_limit: UserSettings['postListLimit']
post_list_order: UserSettings['postListOrder']
viewed_post_display: UserSettings['viewedPostDisplay']
tag_autocomplete_nico: UserSettings['tagAutocompleteNico']
auto_fetch_title: UserSettings['autoFetchTitle']
auto_fetch_thumbnail: UserSettings['autoFetchThumbnail']
wiki_editor_mode: UserSettings['wikiEditorMode']
theme: UserSettings['theme']
auto_fetch_title: UserSettings['autoFetchTitle']
auto_fetch_thumbnail: UserSettings['autoFetchThumbnail']
wiki_editor_mode: UserSettings['wikiEditorMode']
}>,
): Promise<UserSettings> => await apiPatch<UserSettings> ('/users/settings', settings)
export const toFetchPostsOrder = (
value: UserSettings['postListOrder'],
): FetchPostsOrder =>
value.replace (/_(asc|desc)$/, ':$1') as FetchPostsOrder
const safeParseClientSettings = (raw: string | null): ClientSettings => {
if (!(raw))
return { }
@@ -149,6 +132,45 @@ export const updateClientSettings = (
}
const validListLimit = (value: unknown): ClientListLimit | null =>
value === 20 || value === 50 || value === 100 ? value : null
export const getClientListLimit = (
listKey: ClientListKey,
): ClientListLimit | null => {
const value = loadClientSettings ().lists?.[listKey]?.limit
return validListLimit (value)
}
export const getClientListOrder = <T extends string>(
listKey: ClientListKey,
): T | null => {
const value = loadClientSettings ().lists?.[listKey]?.order
return typeof value === 'string' ? value as T : null
}
export const setClientListSettings = (
listKey: ClientListKey,
{ limit, order }: { limit?: ClientListLimit
order?: string },
): void => {
updateClientSettings (settings => ({
...settings,
lists: {
...(settings.lists ?? { }),
[listKey]: {
...(settings.lists?.[listKey] ?? { }),
...(limit == null ? { } : { limit }),
...(order == null ? { } : { order }),
},
},
}))
}
const legacyTheatreLayoutMode = (): TheatreLayoutMode | null => {
const value = localStorage.getItem (LEGACY_THEATRE_LAYOUT_STORAGE_KEY)
return (