Mayo commited on
Commit
137382f
·
unverified ·
1 Parent(s): e4afd8c

feat: persist font family

Browse files
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={!hasBlocks || fontOptions.length === 0}
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>((set) => ({
745
- brushConfig: {
746
- size: 36,
747
- color: '#ffffff',
748
- },
749
- setBrushConfig: (config) =>
750
- set((state) => ({
751
- brushConfig: { ...state.brushConfig, ...config },
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
+ )