bindery-cli 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/bindery/assets/cover.html +892 -0
- data/lib/bindery/assets/index.html +165 -618
- data/lib/bindery/cli.rb +11 -27
- data/lib/bindery/generators/templates/project/README.md.erb +1 -4
- data/lib/bindery/server.rb +21 -63
- data/lib/bindery/version.rb +1 -1
- metadata +2 -1
|
@@ -3,685 +3,232 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title
|
|
6
|
+
<title>书库</title>
|
|
7
7
|
<style>
|
|
8
8
|
* {
|
|
9
9
|
box-sizing: border-box;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
11
|
:root {
|
|
13
12
|
--paper: #ffffff;
|
|
14
|
-
--panel: #ffffff;
|
|
15
13
|
--ink: #262220;
|
|
16
14
|
--ink-soft: #7a7266;
|
|
17
15
|
--rule: #d8d1bd;
|
|
18
16
|
--seal: #a23b2e;
|
|
19
17
|
--seal-dark: #7e2c22;
|
|
20
|
-
--seal-tint: rgba(162, 59, 46, 0.14);
|
|
21
18
|
}
|
|
22
|
-
|
|
23
|
-
html,
|
|
24
|
-
body {
|
|
25
|
-
height: 100%;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
19
|
body {
|
|
29
20
|
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
|
30
21
|
margin: 0;
|
|
31
|
-
display: flex;
|
|
32
|
-
flex-direction: column;
|
|
33
|
-
overflow: hidden;
|
|
34
22
|
background: var(--paper);
|
|
35
23
|
color: var(--ink);
|
|
36
24
|
}
|
|
37
|
-
|
|
38
|
-
::selection {
|
|
39
|
-
background: var(--seal-tint);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
:focus-visible {
|
|
43
|
-
outline: 2px solid var(--seal);
|
|
44
|
-
outline-offset: 2px;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.layout {
|
|
48
|
-
flex: 1;
|
|
49
|
-
min-height: 0;
|
|
50
|
-
display: flex;
|
|
51
|
-
gap: 32px;
|
|
52
|
-
padding: 24px 28px;
|
|
53
|
-
max-width: 1100px;
|
|
54
|
-
width: 100%;
|
|
55
|
-
margin: 0 auto;
|
|
56
|
-
align-items: stretch;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.panel {
|
|
60
|
-
background: transparent;
|
|
61
|
-
padding: 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.controls {
|
|
65
|
-
flex: 3 1 0;
|
|
66
|
-
min-width: 0;
|
|
67
|
-
overflow-y: auto;
|
|
68
|
-
padding-right: 28px;
|
|
69
|
-
border-right: 1px solid var(--rule);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.preview {
|
|
73
|
-
flex: 2 1 0;
|
|
74
|
-
min-width: 0;
|
|
25
|
+
header {
|
|
75
26
|
display: flex;
|
|
76
|
-
flex-direction: column;
|
|
77
27
|
align-items: center;
|
|
78
|
-
justify-content:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.section {
|
|
83
|
-
margin-bottom: 16px;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
padding: 20px 32px;
|
|
30
|
+
border-bottom: 1px solid var(--rule);
|
|
84
31
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
32
|
+
header h1 {
|
|
33
|
+
margin: 0;
|
|
34
|
+
font-size: 20px;
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
font-family: "Songti SC", "STSong", serif;
|
|
37
|
+
letter-spacing: 0.02em;
|
|
88
38
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
39
|
+
.nav a {
|
|
40
|
+
display: inline-block;
|
|
41
|
+
padding: 8px 16px;
|
|
42
|
+
border-radius: 3px;
|
|
43
|
+
background: var(--seal);
|
|
44
|
+
color: #fff;
|
|
45
|
+
text-decoration: none;
|
|
46
|
+
font-size: 13.5px;
|
|
47
|
+
font-weight: 600;
|
|
93
48
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
margin-top: 16px;
|
|
49
|
+
.nav a:hover {
|
|
50
|
+
background: var(--seal-dark);
|
|
97
51
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.row > .field-options {
|
|
105
|
-
flex: 0 0 110px;
|
|
106
|
-
min-width: 0;
|
|
52
|
+
main {
|
|
53
|
+
max-width: 760px;
|
|
54
|
+
margin: 0 auto;
|
|
55
|
+
padding: 24px 32px 48px;
|
|
107
56
|
}
|
|
108
|
-
|
|
109
|
-
label {
|
|
110
|
-
display: block;
|
|
111
|
-
font-size: 12.5px;
|
|
57
|
+
.summary {
|
|
112
58
|
color: var(--ink-soft);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
label:first-child {
|
|
117
|
-
margin-top: 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
input[type="text"],
|
|
121
|
-
select {
|
|
122
|
-
width: 100%;
|
|
123
|
-
padding: 7px 2px 8px;
|
|
124
|
-
border: none;
|
|
125
|
-
border-bottom: 1px solid var(--rule);
|
|
126
|
-
border-radius: 0;
|
|
127
|
-
font-size: 14px;
|
|
128
|
-
background: transparent;
|
|
129
|
-
color: var(--ink);
|
|
130
|
-
font-family: inherit;
|
|
131
|
-
transition:
|
|
132
|
-
border-color 0.15s,
|
|
133
|
-
box-shadow 0.15s;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
input[type="text"]::placeholder {
|
|
137
|
-
color: #b7ae9c;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
select {
|
|
141
|
-
appearance: none;
|
|
142
|
-
-webkit-appearance: none;
|
|
143
|
-
background-image:
|
|
144
|
-
linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%),
|
|
145
|
-
linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
|
|
146
|
-
background-position:
|
|
147
|
-
calc(100% - 6px) center,
|
|
148
|
-
calc(100% - 1px) center;
|
|
149
|
-
background-size:
|
|
150
|
-
6px 6px,
|
|
151
|
-
6px 6px;
|
|
152
|
-
background-repeat: no-repeat;
|
|
153
|
-
padding-right: 18px;
|
|
154
|
-
cursor: pointer;
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
margin: 0 0 8px;
|
|
155
61
|
}
|
|
156
|
-
|
|
157
|
-
input[type="text"]:focus,
|
|
158
|
-
select:focus {
|
|
159
|
-
outline: none;
|
|
160
|
-
border-bottom: 1px solid var(--accent, var(--seal));
|
|
161
|
-
box-shadow: 0 1px 0 0 var(--accent, var(--seal));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.swatches {
|
|
62
|
+
.book {
|
|
165
63
|
display: flex;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.swatch {
|
|
172
|
-
width: 30px;
|
|
173
|
-
height: 30px;
|
|
174
|
-
border-radius: 50%;
|
|
175
|
-
cursor: pointer;
|
|
176
|
-
border: 2px solid transparent;
|
|
177
|
-
position: relative;
|
|
178
|
-
transition:
|
|
179
|
-
box-shadow 0.15s,
|
|
180
|
-
transform 0.1s;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.swatch:hover {
|
|
184
|
-
transform: translateY(-1px);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.swatch.active {
|
|
188
|
-
box-shadow:
|
|
189
|
-
0 0 0 2px var(--panel),
|
|
190
|
-
0 0 0 4px var(--ink);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.swatch.active::after {
|
|
194
|
-
content: none;
|
|
64
|
+
gap: 16px;
|
|
65
|
+
padding: 16px 0;
|
|
66
|
+
border-bottom: 1px solid #f0f0f0;
|
|
67
|
+
align-items: center;
|
|
195
68
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
border: none;
|
|
199
|
-
margin: 0;
|
|
69
|
+
.book:last-child {
|
|
70
|
+
border-bottom: none;
|
|
200
71
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
72
|
+
.cover {
|
|
73
|
+
width: 64px;
|
|
74
|
+
height: 102px;
|
|
75
|
+
object-fit: cover;
|
|
76
|
+
border-radius: 3px;
|
|
77
|
+
background: #f5f5f5;
|
|
78
|
+
border: 1px solid var(--rule);
|
|
79
|
+
flex: none;
|
|
208
80
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
0 0 0 2px var(--panel),
|
|
213
|
-
0 0 0 4px var(--ink);
|
|
81
|
+
.info {
|
|
82
|
+
flex: 1;
|
|
83
|
+
min-width: 0;
|
|
214
84
|
}
|
|
215
|
-
|
|
216
|
-
.swatch.custom input[type="color"] {
|
|
217
|
-
position: absolute;
|
|
218
|
-
inset: 0;
|
|
219
|
-
width: 100%;
|
|
220
|
-
height: 100%;
|
|
221
|
-
opacity: 0;
|
|
222
|
-
cursor: pointer;
|
|
223
|
-
border: none;
|
|
224
|
-
padding: 0;
|
|
85
|
+
.info h2 {
|
|
225
86
|
margin: 0;
|
|
87
|
+
font-size: 16px;
|
|
226
88
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
width: auto;
|
|
232
|
-
height: auto;
|
|
233
|
-
display: block;
|
|
234
|
-
border-radius: 2px;
|
|
235
|
-
border: 1px solid var(--rule);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
.actions {
|
|
239
|
-
display: flex;
|
|
240
|
-
gap: 10px;
|
|
89
|
+
.meta {
|
|
90
|
+
color: var(--ink-soft);
|
|
91
|
+
font-size: 13px;
|
|
92
|
+
margin: 4px 0 10px;
|
|
241
93
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
padding:
|
|
245
|
-
border: none;
|
|
94
|
+
.dl {
|
|
95
|
+
display: inline-block;
|
|
96
|
+
padding: 6px 14px;
|
|
246
97
|
border-radius: 3px;
|
|
247
|
-
|
|
248
|
-
cursor: pointer;
|
|
249
|
-
font-weight: 600;
|
|
250
|
-
font-family: inherit;
|
|
251
|
-
transition:
|
|
252
|
-
transform 0.08s,
|
|
253
|
-
background 0.15s,
|
|
254
|
-
opacity 0.15s;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
.primary {
|
|
258
|
-
background: var(--accent, var(--seal));
|
|
98
|
+
background: var(--seal);
|
|
259
99
|
color: #fff;
|
|
100
|
+
text-decoration: none;
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
font-weight: 600;
|
|
260
103
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
filter: brightness(0.85);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
.primary:active {
|
|
267
|
-
transform: translateY(1px);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.secondary {
|
|
271
|
-
background: transparent;
|
|
272
|
-
color: var(--ink);
|
|
273
|
-
border: 1px solid var(--rule);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.secondary:hover {
|
|
277
|
-
border-color: var(--ink-soft);
|
|
104
|
+
.dl:hover {
|
|
105
|
+
background: var(--seal-dark);
|
|
278
106
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
cursor: not-allowed;
|
|
283
|
-
transform: none;
|
|
107
|
+
.not-built {
|
|
108
|
+
color: #999;
|
|
109
|
+
font-size: 13px;
|
|
284
110
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
font-size: 12.5px;
|
|
111
|
+
.empty {
|
|
112
|
+
text-align: center;
|
|
288
113
|
color: var(--ink-soft);
|
|
289
|
-
|
|
114
|
+
padding: 48px 0;
|
|
290
115
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
flex-direction: column;
|
|
295
|
-
overflow-y: auto;
|
|
296
|
-
padding: 20px;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.controls {
|
|
300
|
-
flex: none;
|
|
301
|
-
width: 100%;
|
|
302
|
-
border-right: none;
|
|
303
|
-
border-bottom: 1px solid var(--rule);
|
|
304
|
-
padding-right: 0;
|
|
305
|
-
padding-bottom: 24px;
|
|
306
|
-
overflow-y: visible;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.preview {
|
|
310
|
-
padding: 24px 12px;
|
|
311
|
-
}
|
|
116
|
+
.empty p {
|
|
117
|
+
margin: 8px 0 0;
|
|
118
|
+
font-size: 13px;
|
|
312
119
|
}
|
|
313
120
|
</style>
|
|
314
121
|
</head>
|
|
315
122
|
<body>
|
|
316
|
-
<
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<select id="book">
|
|
321
|
-
<option value="">(无书籍)</option>
|
|
322
|
-
</select>
|
|
323
|
-
</div>
|
|
324
|
-
|
|
325
|
-
<div class="section">
|
|
326
|
-
<div class="row">
|
|
327
|
-
<div class="field">
|
|
328
|
-
<label for="title">书名</label>
|
|
329
|
-
<input type="text" id="title" placeholder="书名" />
|
|
330
|
-
</div>
|
|
331
|
-
<div class="field-options">
|
|
332
|
-
<label for="titleAlign">书名对齐</label>
|
|
333
|
-
<select id="titleAlign">
|
|
334
|
-
<option value="center">居中</option>
|
|
335
|
-
<option value="left">左对齐</option>
|
|
336
|
-
<option value="right">右对齐</option>
|
|
337
|
-
</select>
|
|
338
|
-
</div>
|
|
339
|
-
</div>
|
|
340
|
-
<div class="row">
|
|
341
|
-
<div class="field">
|
|
342
|
-
<label for="subtitle">副标题</label>
|
|
343
|
-
<input type="text" id="subtitle" placeholder="副标题" />
|
|
344
|
-
</div>
|
|
345
|
-
<div class="field-options">
|
|
346
|
-
<label for="placement">副标题位置</label>
|
|
347
|
-
<select id="placement">
|
|
348
|
-
<option value="bottom_right">下方 · 右</option>
|
|
349
|
-
<option value="bottom_left">下方 · 左</option>
|
|
350
|
-
<option value="top_right">上方 · 右</option>
|
|
351
|
-
<option value="top_left">上方 · 左</option>
|
|
352
|
-
</select>
|
|
353
|
-
</div>
|
|
354
|
-
</div>
|
|
355
|
-
<label for="topText">顶部文字</label>
|
|
356
|
-
<input type="text" id="topText" placeholder="一句话介绍这本书" />
|
|
357
|
-
<label for="author">作者</label>
|
|
358
|
-
<input type="text" id="author" placeholder="作者" />
|
|
359
|
-
</div>
|
|
360
|
-
|
|
361
|
-
<div class="section">
|
|
362
|
-
<label for="font">字体</label>
|
|
363
|
-
<select id="font"></select>
|
|
364
|
-
<label for="logo">LOGO</label>
|
|
365
|
-
<input type="text" id="logo" value="再读经典" />
|
|
366
|
-
<label>配色</label>
|
|
367
|
-
<div class="swatches" id="colors"></div>
|
|
368
|
-
</div>
|
|
123
|
+
<header>
|
|
124
|
+
<h1 data-i18n="library">书库</h1>
|
|
125
|
+
<div class="nav">
|
|
126
|
+
<a href="/cover" data-i18n="coverGenerator">封面生成器</a>
|
|
369
127
|
</div>
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
保存到书籍
|
|
376
|
-
</button>
|
|
377
|
-
<button class="secondary" onclick="downloadPng()">下载 PNG</button>
|
|
378
|
-
</div>
|
|
379
|
-
<div id="status"></div>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
128
|
+
</header>
|
|
129
|
+
<main>
|
|
130
|
+
<p class="summary" id="summary"></p>
|
|
131
|
+
<div id="books"></div>
|
|
132
|
+
</main>
|
|
382
133
|
|
|
383
134
|
<script>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
s.title = name;
|
|
424
|
-
s.style.background = hex;
|
|
425
|
-
s.onclick = () => {
|
|
426
|
-
color = hex;
|
|
427
|
-
box
|
|
428
|
-
.querySelectorAll(".swatch")
|
|
429
|
-
.forEach((el) => el.classList.remove("active"));
|
|
430
|
-
s.classList.add("active");
|
|
431
|
-
render();
|
|
432
|
-
};
|
|
433
|
-
box.appendChild(s);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// 自定义颜色入口(放最后)
|
|
437
|
-
const custom = document.createElement("label");
|
|
438
|
-
custom.className = "swatch custom";
|
|
439
|
-
custom.title = "自定义颜色";
|
|
440
|
-
const input = document.createElement("input");
|
|
441
|
-
input.type = "color";
|
|
442
|
-
input.value = color;
|
|
443
|
-
custom.appendChild(input);
|
|
444
|
-
custom.addEventListener("click", (e) => {
|
|
445
|
-
if (e.target === custom) input.click();
|
|
446
|
-
});
|
|
447
|
-
input.addEventListener("input", () => {
|
|
448
|
-
color = input.value;
|
|
449
|
-
box
|
|
450
|
-
.querySelectorAll(".swatch")
|
|
451
|
-
.forEach((el) => el.classList.remove("active"));
|
|
452
|
-
custom.classList.add("active");
|
|
453
|
-
render();
|
|
454
|
-
});
|
|
455
|
-
box.appendChild(custom);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function initFonts() {
|
|
459
|
-
const sel = document.getElementById("font");
|
|
460
|
-
FONTS.forEach(([name, family]) => {
|
|
461
|
-
const opt = document.createElement("option");
|
|
462
|
-
opt.value = family;
|
|
463
|
-
opt.textContent = name;
|
|
464
|
-
sel.appendChild(opt);
|
|
135
|
+
// ---------- 多语言 ----------
|
|
136
|
+
const LANG = (navigator.language || "en").toLowerCase().startsWith("zh")
|
|
137
|
+
? "zh"
|
|
138
|
+
: "en";
|
|
139
|
+
const I18N = {
|
|
140
|
+
zh: {
|
|
141
|
+
library: "书库",
|
|
142
|
+
coverGenerator: "封面生成器",
|
|
143
|
+
totalBooks: "共 {n} 本书",
|
|
144
|
+
download: "下载 EPUB",
|
|
145
|
+
notBuilt: "未构建",
|
|
146
|
+
noBooks: "暂无书籍",
|
|
147
|
+
noBooksHint: "运行 bindery new book 创建第一本书",
|
|
148
|
+
loadError: "加载书库失败",
|
|
149
|
+
},
|
|
150
|
+
en: {
|
|
151
|
+
library: "Library",
|
|
152
|
+
coverGenerator: "Cover generator",
|
|
153
|
+
totalBooks: "{n} books",
|
|
154
|
+
download: "Download EPUB",
|
|
155
|
+
notBuilt: "Not built",
|
|
156
|
+
noBooks: "No books yet",
|
|
157
|
+
noBooksHint: "Run bindery new book to create your first book",
|
|
158
|
+
loadError: "Failed to load library",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const t = (key) => I18N[LANG][key] || I18N.en[key] || key;
|
|
162
|
+
|
|
163
|
+
function esc(s) {
|
|
164
|
+
return String(s)
|
|
165
|
+
.replace(/&/g, "&")
|
|
166
|
+
.replace(/</g, "<")
|
|
167
|
+
.replace(/>/g, ">")
|
|
168
|
+
.replace(/"/g, """);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function applyI18n() {
|
|
172
|
+
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
|
173
|
+
el.textContent = t(el.dataset.i18n);
|
|
465
174
|
});
|
|
466
|
-
sel.value = font;
|
|
467
175
|
}
|
|
468
176
|
|
|
469
|
-
function wrapText(text, maxWidth, font) {
|
|
470
|
-
ctx.font = font;
|
|
471
|
-
if (!text) return [""];
|
|
472
|
-
const words = text.split(/\s+/).filter(Boolean);
|
|
473
|
-
const lines = [];
|
|
474
|
-
if (words.length <= 1) {
|
|
475
|
-
let line = "";
|
|
476
|
-
for (const ch of text) {
|
|
477
|
-
const test = line + ch;
|
|
478
|
-
if (line && ctx.measureText(test).width > maxWidth) {
|
|
479
|
-
lines.push(line);
|
|
480
|
-
line = ch;
|
|
481
|
-
} else line = test;
|
|
482
|
-
}
|
|
483
|
-
if (line) lines.push(line);
|
|
484
|
-
} else {
|
|
485
|
-
let line = "";
|
|
486
|
-
for (const w of words) {
|
|
487
|
-
const test = line ? line + " " + w : w;
|
|
488
|
-
if (line && ctx.measureText(test).width > maxWidth) {
|
|
489
|
-
lines.push(line);
|
|
490
|
-
line = w;
|
|
491
|
-
} else line = test;
|
|
492
|
-
}
|
|
493
|
-
if (line) lines.push(line);
|
|
494
|
-
}
|
|
495
|
-
return lines;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// 书名自动适应字号
|
|
499
|
-
function fitTitle(text, maxWidth) {
|
|
500
|
-
let size = 76;
|
|
501
|
-
while (size > 30) {
|
|
502
|
-
const lines = wrapText(text, maxWidth, `400 ${size}px ${font}`);
|
|
503
|
-
if (lines.length <= 4) return { lines, size };
|
|
504
|
-
size -= 4;
|
|
505
|
-
}
|
|
506
|
-
const lines = wrapText(text, maxWidth, `400 30px ${font}`);
|
|
507
|
-
return { lines, size: 30 };
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function render() {
|
|
511
|
-
const topText = document.getElementById("topText").value;
|
|
512
|
-
const title = document.getElementById("title").value;
|
|
513
|
-
const subtitle = document.getElementById("subtitle").value;
|
|
514
|
-
const author = document.getElementById("author").value;
|
|
515
|
-
const placement = document.getElementById("placement").value;
|
|
516
|
-
font = document.getElementById("font").value;
|
|
517
|
-
|
|
518
|
-
// 保存按钮颜色随选中配色同步
|
|
519
|
-
document.documentElement.style.setProperty("--accent", color);
|
|
520
|
-
|
|
521
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
522
|
-
ctx.fillStyle = "#fff";
|
|
523
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
524
|
-
|
|
525
|
-
// 顶部色条
|
|
526
|
-
ctx.fillStyle = color;
|
|
527
|
-
ctx.fillRect(20 * S, 0, 460 * S, 10 * S);
|
|
528
|
-
|
|
529
|
-
// 顶部文字(一句话介绍)
|
|
530
|
-
ctx.fillStyle = "#000";
|
|
531
|
-
ctx.textAlign = "center";
|
|
532
|
-
ctx.textBaseline = "top";
|
|
533
|
-
ctx.font = `400 ${20 * S}px ${font}`;
|
|
534
|
-
ctx.fillText(topText, 250 * S, 14 * S, 460 * S);
|
|
535
|
-
|
|
536
|
-
drawTextLayer();
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function drawTextLayer() {
|
|
540
|
-
const logo = document.getElementById("logo").value;
|
|
541
|
-
const title = document.getElementById("title").value;
|
|
542
|
-
const subtitle = document.getElementById("subtitle").value;
|
|
543
|
-
const author = document.getElementById("author").value;
|
|
544
|
-
const placement = document.getElementById("placement").value;
|
|
545
|
-
|
|
546
|
-
// 书名块
|
|
547
|
-
const maxW = 420; // 460 - 2*20 边距
|
|
548
|
-
const { lines, size } = fitTitle(title, maxW);
|
|
549
|
-
const lineHeight = size * 1.1;
|
|
550
|
-
const pad = 25;
|
|
551
|
-
const blockH = lines.length * lineHeight + pad * 2;
|
|
552
|
-
const titleTop = Math.round((800 - blockH) / 2);
|
|
553
|
-
|
|
554
|
-
ctx.fillStyle = color;
|
|
555
|
-
ctx.fillRect(20 * S, titleTop * S, 460 * S, blockH * S);
|
|
556
|
-
ctx.fillStyle = "#fff";
|
|
557
|
-
ctx.textBaseline = "top";
|
|
558
|
-
ctx.font = `400 ${size * S}px ${font}`;
|
|
559
|
-
|
|
560
|
-
const titleAlign = document.getElementById("titleAlign").value;
|
|
561
|
-
let titleX;
|
|
562
|
-
if (titleAlign === "left") {
|
|
563
|
-
ctx.textAlign = "left";
|
|
564
|
-
titleX = 40 * S;
|
|
565
|
-
} else if (titleAlign === "right") {
|
|
566
|
-
ctx.textAlign = "right";
|
|
567
|
-
titleX = 460 * S;
|
|
568
|
-
} else {
|
|
569
|
-
ctx.textAlign = "center";
|
|
570
|
-
titleX = 250 * S;
|
|
571
|
-
}
|
|
572
|
-
lines.forEach((ln, i) => {
|
|
573
|
-
ctx.fillText(ln, titleX, (titleTop + pad + i * lineHeight) * S);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
// 副标题
|
|
577
|
-
const subBelowY = titleTop + blockH + 8;
|
|
578
|
-
const subAboveY = titleTop - 34 - 8;
|
|
579
|
-
ctx.fillStyle = "#000";
|
|
580
|
-
ctx.font = `400 ${34 * S}px ${font}`;
|
|
581
|
-
if (placement === "top_left") {
|
|
582
|
-
ctx.textAlign = "left";
|
|
583
|
-
ctx.fillText(subtitle, 20 * S, subAboveY * S);
|
|
584
|
-
} else if (placement === "top_right") {
|
|
585
|
-
ctx.textAlign = "right";
|
|
586
|
-
ctx.fillText(subtitle, 480 * S, subAboveY * S);
|
|
587
|
-
} else if (placement === "bottom_left") {
|
|
588
|
-
ctx.textAlign = "left";
|
|
589
|
-
ctx.fillText(subtitle, 20 * S, subBelowY * S);
|
|
590
|
-
} else {
|
|
591
|
-
ctx.textAlign = "right";
|
|
592
|
-
ctx.fillText(subtitle, 480 * S, subBelowY * S);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// LOGO(左下角)
|
|
596
|
-
ctx.fillStyle = "#000";
|
|
597
|
-
ctx.textAlign = "left";
|
|
598
|
-
ctx.textBaseline = "top";
|
|
599
|
-
ctx.font = `400 ${28 * S}px ${font}`;
|
|
600
|
-
ctx.fillText(logo, 20 * S, (800 - 20 - 28) * S);
|
|
601
|
-
|
|
602
|
-
// 作者(右下)
|
|
603
|
-
ctx.fillStyle = "#000";
|
|
604
|
-
ctx.textAlign = "right";
|
|
605
|
-
ctx.font = `400 ${24 * S}px ${font}`;
|
|
606
|
-
ctx.fillText(author, 480 * S, (800 - 20 - 24) * S);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// 加载书籍列表并自动填充
|
|
610
177
|
async function loadBooks() {
|
|
178
|
+
const box = document.getElementById("books");
|
|
179
|
+
const summary = document.getElementById("summary");
|
|
611
180
|
try {
|
|
612
181
|
const res = await fetch("/books.json");
|
|
613
182
|
const data = await res.json();
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const result = await res.json();
|
|
656
|
-
status(
|
|
657
|
-
result.ok
|
|
658
|
-
? "✅ 封面已保存到书籍 " + bookId
|
|
659
|
-
: "❌ " + (result.error || "保存失败"),
|
|
660
|
-
);
|
|
183
|
+
const books = data.books || [];
|
|
184
|
+
summary.textContent = t("totalBooks").replace("{n}", books.length);
|
|
185
|
+
if (books.length === 0) {
|
|
186
|
+
box.innerHTML =
|
|
187
|
+
'<div class="empty"><p>' +
|
|
188
|
+
t("noBooks") +
|
|
189
|
+
"</p><p>" +
|
|
190
|
+
t("noBooksHint") +
|
|
191
|
+
"</p></div>";
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
box.innerHTML = books
|
|
195
|
+
.map((b) => {
|
|
196
|
+
const id = esc(b.id || "");
|
|
197
|
+
const title = esc(b.title || "");
|
|
198
|
+
const meta = [b.author, b.dynasty, b.category]
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.map(esc)
|
|
201
|
+
.join(" · ");
|
|
202
|
+
const action = b.epub
|
|
203
|
+
? '<a class="dl" href="/epub/' +
|
|
204
|
+
id +
|
|
205
|
+
'.epub" download>' +
|
|
206
|
+
t("download") +
|
|
207
|
+
"</a>"
|
|
208
|
+
: '<span class="not-built">' + t("notBuilt") + "</span>";
|
|
209
|
+
return (
|
|
210
|
+
'<div class="book"><img class="cover" src="/cover/' +
|
|
211
|
+
id +
|
|
212
|
+
'" alt="' +
|
|
213
|
+
title +
|
|
214
|
+
'"><div class="info"><h2>' +
|
|
215
|
+
title +
|
|
216
|
+
'</h2><p class="meta">' +
|
|
217
|
+
meta +
|
|
218
|
+
"</p>" +
|
|
219
|
+
action +
|
|
220
|
+
"</div></div>"
|
|
221
|
+
);
|
|
222
|
+
})
|
|
223
|
+
.join("");
|
|
661
224
|
} catch (e) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
btn.disabled = false;
|
|
225
|
+
box.innerHTML =
|
|
226
|
+
'<div class="empty"><p>' + t("loadError") + "</p></div>";
|
|
665
227
|
}
|
|
666
228
|
}
|
|
667
229
|
|
|
668
|
-
|
|
669
|
-
const a = document.createElement("a");
|
|
670
|
-
a.download = "cover.png";
|
|
671
|
-
a.href = canvas.toDataURL("image/png");
|
|
672
|
-
a.click();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
document.querySelectorAll("input[type=text], select").forEach((el) => {
|
|
676
|
-
if (el.id !== "book") {
|
|
677
|
-
el.addEventListener("input", render);
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
initSwatches();
|
|
682
|
-
initFonts();
|
|
230
|
+
applyI18n();
|
|
683
231
|
loadBooks();
|
|
684
|
-
render();
|
|
685
232
|
</script>
|
|
686
233
|
</body>
|
|
687
234
|
</html>
|