Mayo commited on
feat: persist font family
Browse files- koharu-mcp/src/lib.rs +2 -0
- koharu-mcp/src/params.rs +4 -0
- koharu-pipeline/src/operations.rs +2 -0
- koharu-pipeline/src/pipeline.rs +3 -0
- koharu-renderer/src/facade.rs +13 -2
- ui/components/canvas/CanvasToolbar.tsx +10 -6
- ui/lib/rpc-types.ts +2 -0
- ui/lib/store.ts +26 -10
koharu-mcp/src/lib.rs
CHANGED
|
@@ -373,6 +373,7 @@ impl KoharuMcp {
|
|
| 373 |
index: p.index,
|
| 374 |
text_block_index: p.text_block_index,
|
| 375 |
shader_effect: effect,
|
|
|
|
| 376 |
},
|
| 377 |
)
|
| 378 |
.await
|
|
@@ -451,6 +452,7 @@ impl KoharuMcp {
|
|
| 451 |
llm_model_id: p.llm_model_id,
|
| 452 |
language: p.language,
|
| 453 |
shader_effect: effect,
|
|
|
|
| 454 |
},
|
| 455 |
)
|
| 456 |
.await
|
|
|
|
| 373 |
index: p.index,
|
| 374 |
text_block_index: p.text_block_index,
|
| 375 |
shader_effect: effect,
|
| 376 |
+
font_family: p.font_family,
|
| 377 |
},
|
| 378 |
)
|
| 379 |
.await
|
|
|
|
| 452 |
llm_model_id: p.llm_model_id,
|
| 453 |
language: p.language,
|
| 454 |
shader_effect: effect,
|
| 455 |
+
font_family: p.font_family,
|
| 456 |
},
|
| 457 |
)
|
| 458 |
.await
|
koharu-mcp/src/params.rs
CHANGED
|
@@ -49,6 +49,8 @@ pub(crate) struct RenderParams {
|
|
| 49 |
pub text_block_index: Option<usize>,
|
| 50 |
/// Shader effect: "normal", "antique", "metal", "manga", "motionblur"
|
| 51 |
pub shader_effect: Option<String>,
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
#[derive(Deserialize, JsonSchema)]
|
|
@@ -77,6 +79,8 @@ pub(crate) struct ProcessParams {
|
|
| 77 |
pub language: Option<String>,
|
| 78 |
/// Shader effect
|
| 79 |
pub shader_effect: Option<String>,
|
|
|
|
|
|
|
| 80 |
}
|
| 81 |
|
| 82 |
#[derive(Deserialize, JsonSchema)]
|
|
|
|
| 49 |
pub text_block_index: Option<usize>,
|
| 50 |
/// Shader effect: "normal", "antique", "metal", "manga", "motionblur"
|
| 51 |
pub shader_effect: Option<String>,
|
| 52 |
+
/// Selected font family for rendering
|
| 53 |
+
pub font_family: Option<String>,
|
| 54 |
}
|
| 55 |
|
| 56 |
#[derive(Deserialize, JsonSchema)]
|
|
|
|
| 79 |
pub language: Option<String>,
|
| 80 |
/// Shader effect
|
| 81 |
pub shader_effect: Option<String>,
|
| 82 |
+
/// Selected font family for rendering
|
| 83 |
+
pub font_family: Option<String>,
|
| 84 |
}
|
| 85 |
|
| 86 |
#[derive(Deserialize, JsonSchema)]
|
koharu-pipeline/src/operations.rs
CHANGED
|
@@ -509,6 +509,7 @@ pub struct RenderPayload {
|
|
| 509 |
pub index: usize,
|
| 510 |
pub text_block_index: Option<usize>,
|
| 511 |
pub shader_effect: Option<TextShaderEffect>,
|
|
|
|
| 512 |
}
|
| 513 |
|
| 514 |
#[instrument(level = "info", skip_all)]
|
|
@@ -527,6 +528,7 @@ pub async fn render(state: AppResources, payload: RenderPayload) -> anyhow::Resu
|
|
| 527 |
&mut updated,
|
| 528 |
payload.text_block_index,
|
| 529 |
payload.shader_effect.unwrap_or_default(),
|
|
|
|
| 530 |
)?;
|
| 531 |
|
| 532 |
let mut guard = state.state.write().await;
|
|
|
|
| 509 |
pub index: usize,
|
| 510 |
pub text_block_index: Option<usize>,
|
| 511 |
pub shader_effect: Option<TextShaderEffect>,
|
| 512 |
+
pub font_family: Option<String>,
|
| 513 |
}
|
| 514 |
|
| 515 |
#[instrument(level = "info", skip_all)]
|
|
|
|
| 528 |
&mut updated,
|
| 529 |
payload.text_block_index,
|
| 530 |
payload.shader_effect.unwrap_or_default(),
|
| 531 |
+
payload.font_family.as_deref(),
|
| 532 |
)?;
|
| 533 |
|
| 534 |
let mut guard = state.state.write().await;
|
koharu-pipeline/src/pipeline.rs
CHANGED
|
@@ -69,6 +69,8 @@ pub struct ProcessRequest {
|
|
| 69 |
pub language: Option<String>,
|
| 70 |
/// Shader effect for rendering.
|
| 71 |
pub shader_effect: Option<TextShaderEffect>,
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
/// Handle to a running pipeline, used for cancellation.
|
|
@@ -242,6 +244,7 @@ async fn run_pipeline_inner(
|
|
| 242 |
&mut snapshot,
|
| 243 |
None,
|
| 244 |
req.shader_effect.unwrap_or_default(),
|
|
|
|
| 245 |
)?;
|
| 246 |
}
|
| 247 |
}
|
|
|
|
| 69 |
pub language: Option<String>,
|
| 70 |
/// Shader effect for rendering.
|
| 71 |
pub shader_effect: Option<TextShaderEffect>,
|
| 72 |
+
/// Selected font family for rendering.
|
| 73 |
+
pub font_family: Option<String>,
|
| 74 |
}
|
| 75 |
|
| 76 |
/// Handle to a running pipeline, used for cancellation.
|
|
|
|
| 244 |
&mut snapshot,
|
| 245 |
None,
|
| 246 |
req.shader_effect.unwrap_or_default(),
|
| 247 |
+
req.font_family.as_deref(),
|
| 248 |
)?;
|
| 249 |
}
|
| 250 |
}
|
koharu-renderer/src/facade.rs
CHANGED
|
@@ -49,6 +49,7 @@ impl Renderer {
|
|
| 49 |
document: &mut Document,
|
| 50 |
text_block_index: Option<usize>,
|
| 51 |
effect: TextShaderEffect,
|
|
|
|
| 52 |
) -> Result<()> {
|
| 53 |
let mut text_blocks = match text_block_index {
|
| 54 |
Some(index) => document
|
|
@@ -60,7 +61,7 @@ impl Renderer {
|
|
| 60 |
};
|
| 61 |
|
| 62 |
text_blocks.par_iter_mut().for_each(|text_block| {
|
| 63 |
-
let _ = self.render_text_block(text_block, effect);
|
| 64 |
});
|
| 65 |
|
| 66 |
if let Some(inpainted) = &document.inpainted
|
|
@@ -93,6 +94,7 @@ impl Renderer {
|
|
| 93 |
&self,
|
| 94 |
text_block: &mut TextBlock,
|
| 95 |
effect: TextShaderEffect,
|
|
|
|
| 96 |
) -> Result<()> {
|
| 97 |
let Some(translation) = &text_block.translation else {
|
| 98 |
return Ok(());
|
|
@@ -101,12 +103,21 @@ impl Renderer {
|
|
| 101 |
return Ok(());
|
| 102 |
};
|
| 103 |
|
| 104 |
-
let style = text_block.style.clone().unwrap_or_else(|| TextStyle {
|
| 105 |
font_families: fonts_for_script(translation),
|
| 106 |
font_size: None,
|
| 107 |
color: [0, 0, 0, 255],
|
| 108 |
effect: None,
|
| 109 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
let font = self.select_font(&style)?;
|
| 111 |
let block_effect = style.effect.unwrap_or(effect);
|
| 112 |
let color = text_block
|
|
|
|
| 49 |
document: &mut Document,
|
| 50 |
text_block_index: Option<usize>,
|
| 51 |
effect: TextShaderEffect,
|
| 52 |
+
font_family: Option<&str>,
|
| 53 |
) -> Result<()> {
|
| 54 |
let mut text_blocks = match text_block_index {
|
| 55 |
Some(index) => document
|
|
|
|
| 61 |
};
|
| 62 |
|
| 63 |
text_blocks.par_iter_mut().for_each(|text_block| {
|
| 64 |
+
let _ = self.render_text_block(text_block, effect, font_family);
|
| 65 |
});
|
| 66 |
|
| 67 |
if let Some(inpainted) = &document.inpainted
|
|
|
|
| 94 |
&self,
|
| 95 |
text_block: &mut TextBlock,
|
| 96 |
effect: TextShaderEffect,
|
| 97 |
+
font_family: Option<&str>,
|
| 98 |
) -> Result<()> {
|
| 99 |
let Some(translation) = &text_block.translation else {
|
| 100 |
return Ok(());
|
|
|
|
| 103 |
return Ok(());
|
| 104 |
};
|
| 105 |
|
| 106 |
+
let mut style = text_block.style.clone().unwrap_or_else(|| TextStyle {
|
| 107 |
font_families: fonts_for_script(translation),
|
| 108 |
font_size: None,
|
| 109 |
color: [0, 0, 0, 255],
|
| 110 |
effect: None,
|
| 111 |
});
|
| 112 |
+
|
| 113 |
+
if let Some(ff) = font_family {
|
| 114 |
+
if !style.font_families.contains(&ff.to_string()) {
|
| 115 |
+
style.font_families.insert(0, ff.to_string());
|
| 116 |
+
} else {
|
| 117 |
+
style.font_families.retain(|x| x != ff);
|
| 118 |
+
style.font_families.insert(0, ff.to_string());
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
let font = self.select_font(&style)?;
|
| 122 |
let block_effect = style.effect.unwrap_or(effect);
|
| 123 |
let color = text_block
|
ui/components/canvas/CanvasToolbar.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
| 12 |
LanguagesIcon,
|
| 13 |
} from 'lucide-react'
|
| 14 |
import { Separator } from '@/components/ui/separator'
|
| 15 |
-
import { useAppStore } from '@/lib/store'
|
| 16 |
import { useTextBlocks } from '@/hooks/useTextBlocks'
|
| 17 |
import { RenderEffect, RgbaColor, TextStyle } from '@/types'
|
| 18 |
import { Button } from '@/components/ui/button'
|
|
@@ -154,6 +154,7 @@ export function RenderControls() {
|
|
| 154 |
availableFonts,
|
| 155 |
fetchAvailableFonts,
|
| 156 |
} = useAppStore()
|
|
|
|
| 157 |
const { textBlocks, selectedBlockIndex, replaceBlock } = useTextBlocks()
|
| 158 |
const { t } = useTranslation()
|
| 159 |
const selectedBlock =
|
|
@@ -169,11 +170,13 @@ export function RenderControls() {
|
|
| 169 |
availableFonts.length > 0
|
| 170 |
? availableFonts
|
| 171 |
: [
|
|
|
|
| 172 |
...(selectedBlock?.style?.fontFamilies?.slice(0, 1) ?? []),
|
| 173 |
...DEFAULT_FONT_FAMILIES,
|
| 174 |
]
|
| 175 |
const fontOptions = uniqueStrings(fontCandidates)
|
| 176 |
const currentFont =
|
|
|
|
| 177 |
selectedBlock?.style?.fontFamilies?.[0] ??
|
| 178 |
firstBlock?.style?.fontFamilies?.[0] ??
|
| 179 |
(hasBlocks ? fallbackFontFamilies[0] : '')
|
|
@@ -236,6 +239,7 @@ export function RenderControls() {
|
|
| 236 |
<Select
|
| 237 |
value={currentFont}
|
| 238 |
onValueChange={(value) => {
|
|
|
|
| 239 |
const nextFamilies = mergeFontFamilies(
|
| 240 |
value,
|
| 241 |
selectedBlock?.style?.fontFamilies,
|
|
@@ -250,7 +254,7 @@ export function RenderControls() {
|
|
| 250 |
}))
|
| 251 |
void updateTextBlocks(nextBlocks)
|
| 252 |
}}
|
| 253 |
-
disabled={
|
| 254 |
>
|
| 255 |
<SelectTrigger
|
| 256 |
size='sm'
|
|
@@ -259,7 +263,7 @@ export function RenderControls() {
|
|
| 259 |
>
|
| 260 |
<SelectValue placeholder={t('render.fontPlaceholder')} />
|
| 261 |
</SelectTrigger>
|
| 262 |
-
<SelectContent>
|
| 263 |
{fontOptions.map((font) => (
|
| 264 |
<SelectItem key={font} value={font} style={{ fontFamily: font }}>
|
| 265 |
{font}
|
|
@@ -299,7 +303,7 @@ export function RenderControls() {
|
|
| 299 |
<SelectTrigger size='sm' className='h-8 w-28 text-sm'>
|
| 300 |
<SelectValue />
|
| 301 |
</SelectTrigger>
|
| 302 |
-
<SelectContent>
|
| 303 |
{effects.map((effect) => (
|
| 304 |
<SelectItem key={effect.value} value={effect.value}>
|
| 305 |
{effect.label}
|
|
@@ -371,7 +375,7 @@ function LlmStatusPopover() {
|
|
| 371 |
<SelectTrigger className='w-full'>
|
| 372 |
<SelectValue placeholder={t('llm.selectPlaceholder')} />
|
| 373 |
</SelectTrigger>
|
| 374 |
-
<SelectContent>
|
| 375 |
{llmModels.map((model) => (
|
| 376 |
<SelectItem key={model.id} value={model.id}>
|
| 377 |
{model.id}
|
|
@@ -389,7 +393,7 @@ function LlmStatusPopover() {
|
|
| 389 |
<SelectTrigger className='w-full'>
|
| 390 |
<SelectValue placeholder={t('llm.languagePlaceholder')} />
|
| 391 |
</SelectTrigger>
|
| 392 |
-
<SelectContent>
|
| 393 |
{activeLanguages.map((language) => (
|
| 394 |
<SelectItem key={language} value={language}>
|
| 395 |
{language}
|
|
|
|
| 12 |
LanguagesIcon,
|
| 13 |
} from 'lucide-react'
|
| 14 |
import { Separator } from '@/components/ui/separator'
|
| 15 |
+
import { useAppStore, useConfigStore } from '@/lib/store'
|
| 16 |
import { useTextBlocks } from '@/hooks/useTextBlocks'
|
| 17 |
import { RenderEffect, RgbaColor, TextStyle } from '@/types'
|
| 18 |
import { Button } from '@/components/ui/button'
|
|
|
|
| 154 |
availableFonts,
|
| 155 |
fetchAvailableFonts,
|
| 156 |
} = useAppStore()
|
| 157 |
+
const { fontFamily, setFontFamily } = useConfigStore()
|
| 158 |
const { textBlocks, selectedBlockIndex, replaceBlock } = useTextBlocks()
|
| 159 |
const { t } = useTranslation()
|
| 160 |
const selectedBlock =
|
|
|
|
| 170 |
availableFonts.length > 0
|
| 171 |
? availableFonts
|
| 172 |
: [
|
| 173 |
+
...(fontFamily ? [fontFamily] : []),
|
| 174 |
...(selectedBlock?.style?.fontFamilies?.slice(0, 1) ?? []),
|
| 175 |
...DEFAULT_FONT_FAMILIES,
|
| 176 |
]
|
| 177 |
const fontOptions = uniqueStrings(fontCandidates)
|
| 178 |
const currentFont =
|
| 179 |
+
fontFamily ??
|
| 180 |
selectedBlock?.style?.fontFamilies?.[0] ??
|
| 181 |
firstBlock?.style?.fontFamilies?.[0] ??
|
| 182 |
(hasBlocks ? fallbackFontFamilies[0] : '')
|
|
|
|
| 239 |
<Select
|
| 240 |
value={currentFont}
|
| 241 |
onValueChange={(value) => {
|
| 242 |
+
setFontFamily(value)
|
| 243 |
const nextFamilies = mergeFontFamilies(
|
| 244 |
value,
|
| 245 |
selectedBlock?.style?.fontFamilies,
|
|
|
|
| 254 |
}))
|
| 255 |
void updateTextBlocks(nextBlocks)
|
| 256 |
}}
|
| 257 |
+
disabled={fontOptions.length === 0}
|
| 258 |
>
|
| 259 |
<SelectTrigger
|
| 260 |
size='sm'
|
|
|
|
| 263 |
>
|
| 264 |
<SelectValue placeholder={t('render.fontPlaceholder')} />
|
| 265 |
</SelectTrigger>
|
| 266 |
+
<SelectContent position='popper'>
|
| 267 |
{fontOptions.map((font) => (
|
| 268 |
<SelectItem key={font} value={font} style={{ fontFamily: font }}>
|
| 269 |
{font}
|
|
|
|
| 303 |
<SelectTrigger size='sm' className='h-8 w-28 text-sm'>
|
| 304 |
<SelectValue />
|
| 305 |
</SelectTrigger>
|
| 306 |
+
<SelectContent position='popper'>
|
| 307 |
{effects.map((effect) => (
|
| 308 |
<SelectItem key={effect.value} value={effect.value}>
|
| 309 |
{effect.label}
|
|
|
|
| 375 |
<SelectTrigger className='w-full'>
|
| 376 |
<SelectValue placeholder={t('llm.selectPlaceholder')} />
|
| 377 |
</SelectTrigger>
|
| 378 |
+
<SelectContent position='popper'>
|
| 379 |
{llmModels.map((model) => (
|
| 380 |
<SelectItem key={model.id} value={model.id}>
|
| 381 |
{model.id}
|
|
|
|
| 393 |
<SelectTrigger className='w-full'>
|
| 394 |
<SelectValue placeholder={t('llm.languagePlaceholder')} />
|
| 395 |
</SelectTrigger>
|
| 396 |
+
<SelectContent position='popper'>
|
| 397 |
{activeLanguages.map((language) => (
|
| 398 |
<SelectItem key={language} value={language}>
|
| 399 |
{language}
|
ui/lib/rpc-types.ts
CHANGED
|
@@ -63,6 +63,7 @@ export type RpcMethodMap = {
|
|
| 63 |
index: number
|
| 64 |
textBlockIndex?: number
|
| 65 |
shaderEffect?: RenderEffect
|
|
|
|
| 66 |
},
|
| 67 |
void,
|
| 68 |
]
|
|
@@ -82,6 +83,7 @@ export type RpcMethodMap = {
|
|
| 82 |
llmModelId?: string
|
| 83 |
language?: string
|
| 84 |
shaderEffect?: RenderEffect
|
|
|
|
| 85 |
},
|
| 86 |
void,
|
| 87 |
]
|
|
|
|
| 63 |
index: number
|
| 64 |
textBlockIndex?: number
|
| 65 |
shaderEffect?: RenderEffect
|
| 66 |
+
fontFamily?: string
|
| 67 |
},
|
| 68 |
void,
|
| 69 |
]
|
|
|
|
| 83 |
llmModelId?: string
|
| 84 |
language?: string
|
| 85 |
shaderEffect?: RenderEffect
|
| 86 |
+
fontFamily?: string
|
| 87 |
},
|
| 88 |
void,
|
| 89 |
]
|
ui/lib/store.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
import { create } from 'zustand'
|
|
|
|
| 4 |
import i18n from '@/lib/i18n'
|
| 5 |
import {
|
| 6 |
invoke,
|
|
@@ -463,6 +464,7 @@ export const useAppStore = create<AppState>((set, get) => {
|
|
| 463 |
await invoke('render', {
|
| 464 |
index,
|
| 465 |
shaderEffect: get().renderEffect,
|
|
|
|
| 466 |
})
|
| 467 |
if (index === get().currentDocumentIndex) {
|
| 468 |
await get().refreshCurrentDocument()
|
|
@@ -478,6 +480,7 @@ export const useAppStore = create<AppState>((set, get) => {
|
|
| 478 |
index,
|
| 479 |
textBlockIndex,
|
| 480 |
shaderEffect: get().renderEffect,
|
|
|
|
| 481 |
})
|
| 482 |
if (index === get().currentDocumentIndex) {
|
| 483 |
await get().refreshCurrentDocument()
|
|
@@ -630,6 +633,7 @@ export const useAppStore = create<AppState>((set, get) => {
|
|
| 630 |
llmModelId: get().llmSelectedModel,
|
| 631 |
language: get().llmSelectedLanguage,
|
| 632 |
shaderEffect: get().renderEffect,
|
|
|
|
| 633 |
})
|
| 634 |
} catch (err) {
|
| 635 |
console.error('Failed to start processing:', err)
|
|
@@ -659,6 +663,7 @@ export const useAppStore = create<AppState>((set, get) => {
|
|
| 659 |
llmModelId: get().llmSelectedModel,
|
| 660 |
language: get().llmSelectedLanguage,
|
| 661 |
shaderEffect: get().renderEffect,
|
|
|
|
| 662 |
})
|
| 663 |
} catch (err) {
|
| 664 |
console.error('Failed to start processing:', err)
|
|
@@ -694,6 +699,8 @@ type ConfigState = {
|
|
| 694 |
color: string
|
| 695 |
}
|
| 696 |
setBrushConfig: (config: Partial<ConfigState['brushConfig']>) => void
|
|
|
|
|
|
|
| 697 |
}
|
| 698 |
|
| 699 |
// Subscribe to pipeline progress at module level so the EventSource is
|
|
@@ -741,13 +748,22 @@ if (typeof window !== 'undefined') {
|
|
| 741 |
})
|
| 742 |
}
|
| 743 |
|
| 744 |
-
export const useConfigStore = create<ConfigState>(
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
import { create } from 'zustand'
|
| 4 |
+
import { persist } from 'zustand/middleware'
|
| 5 |
import i18n from '@/lib/i18n'
|
| 6 |
import {
|
| 7 |
invoke,
|
|
|
|
| 464 |
await invoke('render', {
|
| 465 |
index,
|
| 466 |
shaderEffect: get().renderEffect,
|
| 467 |
+
fontFamily: useConfigStore.getState().fontFamily,
|
| 468 |
})
|
| 469 |
if (index === get().currentDocumentIndex) {
|
| 470 |
await get().refreshCurrentDocument()
|
|
|
|
| 480 |
index,
|
| 481 |
textBlockIndex,
|
| 482 |
shaderEffect: get().renderEffect,
|
| 483 |
+
fontFamily: useConfigStore.getState().fontFamily,
|
| 484 |
})
|
| 485 |
if (index === get().currentDocumentIndex) {
|
| 486 |
await get().refreshCurrentDocument()
|
|
|
|
| 633 |
llmModelId: get().llmSelectedModel,
|
| 634 |
language: get().llmSelectedLanguage,
|
| 635 |
shaderEffect: get().renderEffect,
|
| 636 |
+
fontFamily: useConfigStore.getState().fontFamily,
|
| 637 |
})
|
| 638 |
} catch (err) {
|
| 639 |
console.error('Failed to start processing:', err)
|
|
|
|
| 663 |
llmModelId: get().llmSelectedModel,
|
| 664 |
language: get().llmSelectedLanguage,
|
| 665 |
shaderEffect: get().renderEffect,
|
| 666 |
+
fontFamily: useConfigStore.getState().fontFamily,
|
| 667 |
})
|
| 668 |
} catch (err) {
|
| 669 |
console.error('Failed to start processing:', err)
|
|
|
|
| 699 |
color: string
|
| 700 |
}
|
| 701 |
setBrushConfig: (config: Partial<ConfigState['brushConfig']>) => void
|
| 702 |
+
fontFamily?: string
|
| 703 |
+
setFontFamily: (font: string) => void
|
| 704 |
}
|
| 705 |
|
| 706 |
// Subscribe to pipeline progress at module level so the EventSource is
|
|
|
|
| 748 |
})
|
| 749 |
}
|
| 750 |
|
| 751 |
+
export const useConfigStore = create<ConfigState>()(
|
| 752 |
+
persist(
|
| 753 |
+
(set) => ({
|
| 754 |
+
brushConfig: {
|
| 755 |
+
size: 36,
|
| 756 |
+
color: '#ffffff',
|
| 757 |
+
},
|
| 758 |
+
setBrushConfig: (config) =>
|
| 759 |
+
set((state) => ({
|
| 760 |
+
brushConfig: { ...state.brushConfig, ...config },
|
| 761 |
+
})),
|
| 762 |
+
fontFamily: undefined,
|
| 763 |
+
setFontFamily: (font) => set({ fontFamily: font }),
|
| 764 |
+
}),
|
| 765 |
+
{
|
| 766 |
+
name: 'koharu-config',
|
| 767 |
+
},
|
| 768 |
+
),
|
| 769 |
+
)
|