1- import { useEffect , useRef , useState } from " preact/hooks" ;
2- import Header from " ./components/Header" ;
3- import OptionsDisplay from " ./components/OptionsDisplay" ;
4- import WebFont from " webfontloader" ;
1+ import { useEffect , useRef , useState } from ' preact/hooks' ;
2+ import Header from ' ./components/Header' ;
3+ import OptionsDisplay from ' ./components/OptionsDisplay' ;
4+ import WebFont from ' webfontloader' ;
55
6- export type TextAlign = " left" | " center" | " right" ;
7- export type TextBaseLine = " top" | " middle" | " bottom" ;
6+ export type TextAlign = ' left' | ' center' | ' right' ;
7+ export type TextBaseLine = ' top' | ' middle' | ' bottom' ;
88
99const hexToRgb = ( hex : string ) : [ number , number , number ] => {
1010 const match = hex . match ( / ^ # ( [ a - f \d ] { 2 } ) ( [ a - f \d ] { 2 } ) ( [ a - f \d ] { 2 } ) $ / i) ;
1111 if ( ! match ) return [ 0 , 0 , 0 ] ;
12- return [
13- parseInt ( match [ 1 ] , 16 ) ,
14- parseInt ( match [ 2 ] , 16 ) ,
15- parseInt ( match [ 3 ] , 16 ) ,
16- ] ;
12+ return [ parseInt ( match [ 1 ] , 16 ) , parseInt ( match [ 2 ] , 16 ) , parseInt ( match [ 3 ] , 16 ) ] ;
13+ } ;
14+
15+ const MAX_CANVAS_WIDTH = 1000 ;
16+ const MAX_CANVAS_HEIGHT = 500 ;
17+
18+ const getCanvasSizeFromCustomAspectRatio = ( aw : number , ah : number ) => {
19+ if ( aw <= 0 || ah <= 0 ) return { width : MAX_CANVAS_WIDTH , height : MAX_CANVAS_HEIGHT } ;
20+ const scale = Math . min ( MAX_CANVAS_WIDTH / aw , MAX_CANVAS_HEIGHT / ah ) ;
21+ return {
22+ width : Math . round ( aw * scale ) ,
23+ height : Math . round ( ah * scale ) ,
24+ } ;
25+ } ;
26+
27+ const getDefaultFontSizeFromCustomAspectRatio = (
28+ aspectRatioWidth : number ,
29+ aspectRatioHeight : number
30+ ) => {
31+ const aspectRatio = aspectRatioWidth / aspectRatioHeight ;
32+ if ( aspectRatio >= 1.5 ) return 120 ; // landscape
33+ if ( aspectRatio <= 0.7 ) return 50 ; // portrait
34+ return 80 ; // square-ish
1735} ;
1836
1937export default function App ( ) {
2038 const DEFAULT_TEXT_SIZE = 120 ;
2139 const DEFAULT_BG_DIM = 0.4 ;
2240 const DEFAULT_TEXT_PADDING = 0.05 ;
2341
24- const camvasSizes : Record <
25- " cover" | " poster" ,
42+ const canvasSizes : Record <
43+ ' cover' | ' poster' ,
2644 { width : number ; height : number ; defaultFontSize : number }
2745 > = {
2846 cover : {
@@ -38,21 +56,47 @@ export default function App() {
3856 } ;
3957
4058 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
41- const [ title , setTitle ] = useState ( " Movies" ) ;
59+ const [ title , setTitle ] = useState ( ' Movies' ) ;
4260 const [ image , setImage ] = useState < HTMLImageElement | null > ( null ) ;
4361 const [ textSize , setTextSize ] = useState ( DEFAULT_TEXT_SIZE ) ;
4462 const [ bgDim , setBgDim ] = useState ( DEFAULT_BG_DIM ) ;
45- const [ fontName , setFontName ] = useState ( "Montserrat" ) ;
46- const [ imageType , setImageType ] = useState < "cover" | "poster" > ( "cover" ) ;
47- const [ textColor , setTextColor ] = useState ( "#ffffff" ) ;
48- const [ dimColor , setDimColor ] = useState ( "#000000" ) ;
49- const [ textAlign , setTextAlign ] = useState < TextAlign > ( "center" ) ;
50- const [ textBaseline , setTextBaseline ] = useState < TextBaseLine > ( "middle" ) ;
63+ const [ fontName , setFontName ] = useState ( 'Montserrat' ) ;
64+ const [ imageType , setImageType ] = useState < 'cover' | 'poster' | 'custom' > ( 'cover' ) ;
65+ const [ customAspectRatioWidth , setCustomAspectRatioWidth ] = useState ( 0.75 ) ;
66+ const [ customAspectRatioHeight , setCustomAspectRatioHeight ] = useState ( 1 ) ;
67+ const [ textColor , setTextColor ] = useState ( '#ffffff' ) ;
68+ const [ dimColor , setDimColor ] = useState ( '#000000' ) ;
69+ const [ textAlign , setTextAlign ] = useState < TextAlign > ( 'center' ) ;
70+ const [ textBaseline , setTextBaseline ] = useState < TextBaseLine > ( 'middle' ) ;
5171 const [ textPadding , setTextPadding ] = useState ( DEFAULT_TEXT_PADDING ) ;
5272
53- const getCanvasWidth = ( ) => camvasSizes [ imageType ] . width ;
54- const getCanvasHeight = ( ) => camvasSizes [ imageType ] . height ;
55- const getDefaultFontSize = ( ) => camvasSizes [ imageType ] . defaultFontSize ;
73+ const getCanvasWidth = ( ) => {
74+ if ( imageType === 'custom' )
75+ return getCanvasSizeFromCustomAspectRatio (
76+ customAspectRatioWidth ,
77+ customAspectRatioHeight
78+ ) . width ;
79+ return canvasSizes [ imageType ] . width ;
80+ } ;
81+
82+ const getCanvasHeight = ( ) => {
83+ if ( imageType === 'custom' )
84+ return getCanvasSizeFromCustomAspectRatio (
85+ customAspectRatioWidth ,
86+ customAspectRatioHeight
87+ ) . height ;
88+ return canvasSizes [ imageType ] . height ;
89+ } ;
90+
91+ const getDefaultFontSize = ( ) => {
92+ if ( imageType === 'custom' ) {
93+ return getDefaultFontSizeFromCustomAspectRatio (
94+ customAspectRatioWidth ,
95+ customAspectRatioHeight
96+ ) ;
97+ }
98+ return canvasSizes [ imageType ] . defaultFontSize ;
99+ } ;
56100
57101 const handleImageUpload = ( e : Event ) => {
58102 const input = e . target as HTMLInputElement ;
@@ -77,19 +121,18 @@ export default function App() {
77121 ctx . textAlign = align ;
78122
79123 let x : number = getCanvasWidth ( ) / 2 ;
80- if ( align === "left" ) x = maxWidth * textPadding ;
81- else if ( align === "right" )
82- x = getCanvasWidth ( ) - maxWidth * textPadding ;
124+ if ( align === 'left' ) x = maxWidth * textPadding ;
125+ else if ( align === 'right' ) x = getCanvasWidth ( ) - maxWidth * textPadding ;
83126
84- const words = text . split ( " " ) ;
127+ const words = text . split ( ' ' ) ;
85128 const lines : string [ ] = [ ] ;
86129 let currentLine = words [ 0 ] ;
87130
88131 for ( let i = 1 ; i < words . length ; i ++ ) {
89132 const word = words [ i ] ;
90- const width = ctx . measureText ( currentLine + " " + word ) . width ;
133+ const width = ctx . measureText ( currentLine + ' ' + word ) . width ;
91134 if ( width < maxWidth ) {
92- currentLine += " " + word ;
135+ currentLine += ' ' + word ;
93136 } else {
94137 lines . push ( currentLine ) ;
95138 currentLine = word ;
@@ -101,15 +144,15 @@ export default function App() {
101144
102145 const padding = getCanvasHeight ( ) * textPadding ;
103146 let y : number ;
104- if ( baseline === " top" ) {
147+ if ( baseline === ' top' ) {
105148 y = padding ;
106- ctx . textBaseline = " top" ;
107- } else if ( baseline === " middle" ) {
149+ ctx . textBaseline = ' top' ;
150+ } else if ( baseline === ' middle' ) {
108151 y = getCanvasHeight ( ) / 2 - totalHeight / 2 ;
109- ctx . textBaseline = " top" ;
152+ ctx . textBaseline = ' top' ;
110153 } else {
111154 y = getCanvasHeight ( ) - totalHeight - padding ;
112- ctx . textBaseline = " top" ;
155+ ctx . textBaseline = ' top' ;
113156 }
114157
115158 for ( const line of lines ) {
@@ -119,9 +162,12 @@ export default function App() {
119162 } ;
120163
121164 const drawCanvas = ( img : HTMLImageElement , titleText : string ) => {
165+ console . log ( 'Custom Aspect Ratio:' , customAspectRatioWidth , ':' , customAspectRatioHeight ) ;
166+ console . log ( 'Canvas Size:' , getCanvasWidth ( ) , 'x' , getCanvasHeight ( ) ) ;
167+
122168 const canvas = canvasRef . current ;
123169 if ( ! canvas ) return ;
124- const ctx = canvas . getContext ( "2d" ) ;
170+ const ctx = canvas . getContext ( '2d' ) ;
125171 if ( ! ctx ) return ;
126172
127173 canvas . width = getCanvasWidth ( ) ;
@@ -181,18 +227,18 @@ export default function App() {
181227
182228 const formatTitleForFileName = ( title : string ) => {
183229 return title
184- . replace ( / [ ^ a - z A - Z 0 - 9 \s ] / g, "" )
185- . replace ( / \s + / g, "-" )
230+ . replace ( / [ ^ a - z A - Z 0 - 9 \s ] / g, '' )
231+ . replace ( / \s + / g, '-' )
186232 . toLowerCase ( ) ;
187233 } ;
188234
189235 const downloadImage = ( ) => {
190236 const canvas = canvasRef . current ;
191237 if ( ! canvas ) return ;
192238
193- const link = document . createElement ( "a" ) ;
239+ const link = document . createElement ( 'a' ) ;
194240 link . download = `jellyfin-cover-${ formatTitleForFileName ( title ) } .png` ;
195- link . href = canvas . toDataURL ( " image/png" ) ;
241+ link . href = canvas . toDataURL ( ' image/png' ) ;
196242 link . click ( ) ;
197243 } ;
198244
@@ -212,13 +258,15 @@ export default function App() {
212258 textAlign ,
213259 textBaseline ,
214260 textPadding ,
261+ customAspectRatioWidth ,
262+ customAspectRatioHeight ,
215263 ] ) ;
216264
217265 useEffect ( ( ) => {
218266 if ( ! fontName ) return ;
219267 WebFont . load ( {
220268 google : {
221- families : [ fontName + " :400,700" ] ,
269+ families : [ fontName + ' :400,700' ] ,
222270 } ,
223271 active : ( ) => {
224272 if ( image ) {
@@ -233,12 +281,19 @@ export default function App() {
233281 defaultImg . onload = ( ) => {
234282 setImage ( defaultImg ) ;
235283 } ;
236- defaultImg . src = " /default-bg.webp" ;
284+ defaultImg . src = ' /default-bg.webp' ;
237285 } , [ ] ) ;
238286
239- const handleImageTypeChange = ( type : " cover" | " poster" ) => {
287+ const handleImageTypeChange = ( type : ' cover' | ' poster' | 'custom' ) => {
240288 setImageType ( type ) ;
241- setTextSize ( camvasSizes [ type ] . defaultFontSize ) ;
289+ setTextSize (
290+ type === 'custom'
291+ ? getDefaultFontSizeFromCustomAspectRatio (
292+ customAspectRatioWidth ,
293+ customAspectRatioHeight
294+ )
295+ : canvasSizes [ type ] . defaultFontSize
296+ ) ;
242297 } ;
243298
244299 return (
@@ -254,6 +309,10 @@ export default function App() {
254309 setImage = { handleImageUpload }
255310 imageType = { imageType }
256311 setImageType = { handleImageTypeChange }
312+ customAspectRatioWidth = { customAspectRatioWidth }
313+ setCustomAspectRatioWidth = { setCustomAspectRatioWidth }
314+ customAspectRatioHeight = { customAspectRatioHeight }
315+ setCustomAspectRatioHeight = { setCustomAspectRatioHeight }
257316 bgDim = { bgDim }
258317 setBgDim = { setBgDim }
259318 defaultBgDim = { DEFAULT_BG_DIM }
@@ -275,15 +334,14 @@ export default function App() {
275334
276335 < div
277336 className = {
278- " flex items-center justify-center grow " +
279- ( imageType === " poster" ? " flex-col" : "" )
337+ ' flex items-center justify-center grow max-h-[500px] ' +
338+ ( imageType === ' poster' ? ' flex-col' : '' )
280339 }
281340 >
282341 < canvas
283- className = {
284- "rounded-md border border-input border-solid grow"
285- }
342+ className = { 'rounded-md border border-input border-solid' }
286343 style = { {
344+ maxHeight : '500px' ,
287345 aspectRatio : `${ getCanvasWidth ( ) } / ${ getCanvasHeight ( ) } ` ,
288346 } }
289347 ref = { canvasRef }
0 commit comments