このコミットが含まれているのは:
@@ -0,0 +1,258 @@
|
||||
import { apiGet, apiPatch } from '@/lib/api'
|
||||
|
||||
import type { FetchPostsOrder } from '@/types'
|
||||
|
||||
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'
|
||||
|
||||
export type UserSettings = {
|
||||
theme: 'system' | 'light' | 'dark'
|
||||
displayDensity: 'comfortable' | 'compact'
|
||||
fontSize: 'small' | 'normal' | 'large'
|
||||
postListLimit: 20 | 50 | 100
|
||||
postListOrder: UserPostListOrder
|
||||
viewedPostDisplay: 'show' | 'dim' | 'hide'
|
||||
tagAutocompleteNico: boolean
|
||||
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'
|
||||
|
||||
type ClientPaneSettings = {
|
||||
widthPxByBreakpoint?: Partial<Record<ClientPaneBreakpoint, number>>
|
||||
collapsed?: boolean }
|
||||
|
||||
export type ClientSettings = {
|
||||
panes?: Record<string, ClientPaneSettings>
|
||||
theatre?: {
|
||||
layoutMode?: TheatreLayoutMode
|
||||
tagFlow?: TheatreTagFlow
|
||||
}
|
||||
gekanator?: {
|
||||
backgroundMotion?: GekanatorBackgroundMotionMode
|
||||
}
|
||||
reducedMotion?: string
|
||||
embedAutoLoad?: string
|
||||
thumbnailMode?: string }
|
||||
|
||||
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' }
|
||||
|
||||
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', 'hide'] as const
|
||||
export const AUTO_FETCH_OPTIONS = ['auto', 'manual', 'off'] as const
|
||||
export const WIKI_EDITOR_MODE_OPTIONS = ['split', 'write', 'preview'] as const
|
||||
|
||||
const LEGACY_THEATRE_LAYOUT_STORAGE_KEY = 'theatre-layout-mode'
|
||||
const LEGACY_THEATRE_TAG_FLOW_STORAGE_KEY = 'theatre-tag-flow'
|
||||
const LEGACY_GEKANATOR_BACKGROUND_MOTION_STORAGE_KEY =
|
||||
'gekanator:background-motion:v1'
|
||||
|
||||
|
||||
export const fetchUserSettings = async (): Promise<UserSettings> =>
|
||||
await apiGet<UserSettings> ('/users/settings')
|
||||
|
||||
|
||||
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']
|
||||
}>,
|
||||
): 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 { }
|
||||
|
||||
try
|
||||
{
|
||||
const parsed = JSON.parse (raw)
|
||||
return parsed && typeof parsed === 'object' ? parsed as ClientSettings : { }
|
||||
}
|
||||
catch
|
||||
{
|
||||
return { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const loadClientSettings = (): ClientSettings =>
|
||||
safeParseClientSettings (localStorage.getItem (CLIENT_SETTINGS_STORAGE_KEY))
|
||||
|
||||
|
||||
export const saveClientSettings = (settings: ClientSettings): void => {
|
||||
localStorage.setItem (CLIENT_SETTINGS_STORAGE_KEY, JSON.stringify (settings))
|
||||
}
|
||||
|
||||
|
||||
export const updateClientSettings = (
|
||||
updater: (settings: ClientSettings) => ClientSettings,
|
||||
): ClientSettings => {
|
||||
const next = updater (loadClientSettings ())
|
||||
saveClientSettings (next)
|
||||
return next
|
||||
}
|
||||
|
||||
|
||||
const legacyTheatreLayoutMode = (): TheatreLayoutMode | null => {
|
||||
const value = localStorage.getItem (LEGACY_THEATRE_LAYOUT_STORAGE_KEY)
|
||||
return (
|
||||
value === 'threeColumns'
|
||||
|| value === 'tagsBottom'
|
||||
|| value === 'commentsBottom'
|
||||
) ? value : null
|
||||
}
|
||||
|
||||
|
||||
const legacyTheatreTagFlow = (): TheatreTagFlow | null => {
|
||||
const value = localStorage.getItem (LEGACY_THEATRE_TAG_FLOW_STORAGE_KEY)
|
||||
return (value === 'vertical' || value === 'horizontal') ? value : null
|
||||
}
|
||||
|
||||
|
||||
const legacyGekanatorBackgroundMotion = (): GekanatorBackgroundMotionMode | null => {
|
||||
const value = localStorage.getItem (LEGACY_GEKANATOR_BACKGROUND_MOTION_STORAGE_KEY)
|
||||
return (value === 'on' || value === 'calm' || value === 'off') ? value : null
|
||||
}
|
||||
|
||||
|
||||
export const getClientTheatreLayoutMode = (): TheatreLayoutMode =>
|
||||
loadClientSettings ().theatre?.layoutMode
|
||||
?? legacyTheatreLayoutMode ()
|
||||
?? 'threeColumns'
|
||||
|
||||
|
||||
export const setClientTheatreLayoutMode = (layoutMode: TheatreLayoutMode): void => {
|
||||
updateClientSettings (settings => ({
|
||||
...settings,
|
||||
theatre: { ...(settings.theatre ?? { }), layoutMode } }))
|
||||
}
|
||||
|
||||
|
||||
export const getClientTheatreTagFlow = (): TheatreTagFlow =>
|
||||
loadClientSettings ().theatre?.tagFlow
|
||||
?? legacyTheatreTagFlow ()
|
||||
?? 'vertical'
|
||||
|
||||
|
||||
export const setClientTheatreTagFlow = (tagFlow: TheatreTagFlow): void => {
|
||||
updateClientSettings (settings => ({
|
||||
...settings,
|
||||
theatre: { ...(settings.theatre ?? { }), tagFlow } }))
|
||||
}
|
||||
|
||||
|
||||
export const getClientGekanatorBackgroundMotion =
|
||||
(): GekanatorBackgroundMotionMode =>
|
||||
loadClientSettings ().gekanator?.backgroundMotion
|
||||
?? legacyGekanatorBackgroundMotion ()
|
||||
?? 'on'
|
||||
|
||||
|
||||
export const setClientGekanatorBackgroundMotion = (
|
||||
backgroundMotion: GekanatorBackgroundMotionMode,
|
||||
): void => {
|
||||
updateClientSettings (settings => ({
|
||||
...settings,
|
||||
gekanator: { ...(settings.gekanator ?? { }), backgroundMotion } }))
|
||||
}
|
||||
|
||||
|
||||
export const getClientPaneWidthPx = (
|
||||
paneKey: string,
|
||||
breakpoint: ClientPaneBreakpoint,
|
||||
): number | null => {
|
||||
const value = loadClientSettings ().panes?.[paneKey]?.widthPxByBreakpoint?.[breakpoint]
|
||||
return typeof value === 'number' ? value : null
|
||||
}
|
||||
|
||||
|
||||
export const setClientPaneWidthPx = (
|
||||
paneKey: string,
|
||||
breakpoint: ClientPaneBreakpoint,
|
||||
widthPx: number,
|
||||
): void => {
|
||||
updateClientSettings (settings => ({
|
||||
...settings,
|
||||
panes: {
|
||||
...(settings.panes ?? { }),
|
||||
[paneKey]: {
|
||||
...(settings.panes?.[paneKey] ?? { }),
|
||||
widthPxByBreakpoint: {
|
||||
...(settings.panes?.[paneKey]?.widthPxByBreakpoint ?? { }),
|
||||
[breakpoint]: widthPx,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
export const setClientPaneCollapsed = (
|
||||
paneKey: string,
|
||||
collapsed: boolean,
|
||||
): void => {
|
||||
updateClientSettings (settings => ({
|
||||
...settings,
|
||||
panes: {
|
||||
...(settings.panes ?? { }),
|
||||
[paneKey]: {
|
||||
...(settings.panes?.[paneKey] ?? { }),
|
||||
collapsed,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
新しい課題から参照
ユーザをブロックする