@flecblog/core-nuxt 0.1.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.
Files changed (70) hide show
  1. package/app.vue +172 -0
  2. package/assets/css/base.scss +96 -0
  3. package/composables/api/article.ts +25 -0
  4. package/composables/api/category.ts +19 -0
  5. package/composables/api/comment.ts +25 -0
  6. package/composables/api/createApi.ts +242 -0
  7. package/composables/api/feedback.ts +14 -0
  8. package/composables/api/friend.ts +14 -0
  9. package/composables/api/moment.ts +10 -0
  10. package/composables/api/music.ts +39 -0
  11. package/composables/api/notification.ts +19 -0
  12. package/composables/api/stats.ts +14 -0
  13. package/composables/api/subscribe.ts +13 -0
  14. package/composables/api/sysconfig.ts +9 -0
  15. package/composables/api/tag.ts +19 -0
  16. package/composables/api/theme.ts +9 -0
  17. package/composables/api/upload.ts +13 -0
  18. package/composables/api/user.ts +77 -0
  19. package/composables/useArticle.ts +227 -0
  20. package/composables/useAuth.ts +180 -0
  21. package/composables/useComment.ts +143 -0
  22. package/composables/useDarkMode.ts +29 -0
  23. package/composables/useEmoji.ts +39 -0
  24. package/composables/useFeedback.ts +38 -0
  25. package/composables/useFriendList.ts +100 -0
  26. package/composables/useMermaid.ts +30 -0
  27. package/composables/useMoment.ts +86 -0
  28. package/composables/useMusic.ts +37 -0
  29. package/composables/useSearchState.ts +90 -0
  30. package/composables/useStores.ts +557 -0
  31. package/composables/useSubscribe.ts +13 -0
  32. package/composables/useTheme.ts +61 -0
  33. package/composables/useUser.ts +202 -0
  34. package/layouts/default.vue +1 -0
  35. package/nuxt.config.ts +170 -0
  36. package/package.json +58 -0
  37. package/pages/index.vue +1 -0
  38. package/plugins/console-banner.client.ts +11 -0
  39. package/plugins/custom-code.ts +121 -0
  40. package/plugins/syncThemeMeta.ts +28 -0
  41. package/plugins/tracker.client.ts +107 -0
  42. package/server/plugins/sitemap.ts +170 -0
  43. package/server/routes/atom.xml.ts +21 -0
  44. package/server/routes/manifest.json.ts +45 -0
  45. package/server/routes/rss.xml.ts +21 -0
  46. package/types/article.ts +48 -0
  47. package/types/auth.ts +8 -0
  48. package/types/category.ts +12 -0
  49. package/types/comment.ts +72 -0
  50. package/types/emoji.ts +10 -0
  51. package/types/feedback.ts +31 -0
  52. package/types/friend.ts +63 -0
  53. package/types/markdown.ts +7 -0
  54. package/types/moment.ts +85 -0
  55. package/types/notification.ts +56 -0
  56. package/types/request.ts +30 -0
  57. package/types/stats.ts +38 -0
  58. package/types/sysconfig.ts +2 -0
  59. package/types/tag.ts +11 -0
  60. package/types/theme.ts +26 -0
  61. package/types/upload.ts +5 -0
  62. package/types/user.ts +106 -0
  63. package/utils/avatar.ts +21 -0
  64. package/utils/date.ts +136 -0
  65. package/utils/download.ts +11 -0
  66. package/utils/emoji.ts +90 -0
  67. package/utils/format.ts +42 -0
  68. package/utils/markdown.ts +1458 -0
  69. package/utils/scroll.ts +31 -0
  70. package/utils/upload.ts +57 -0
@@ -0,0 +1,557 @@
1
+ import { getComments, createComment, deleteComment } from '@/composables/api/comment';
2
+ import { getNotifications, markAsRead, markAllAsRead } from '@/composables/api/notification';
3
+ import { getCategories } from '@/composables/api/category';
4
+ import { getTags } from '@/composables/api/tag';
5
+ import { getArticlesForWeb } from '@/composables/api/article';
6
+ import { getSiteStats, getArchiveStats } from '@/composables/api/stats';
7
+ import type { Article } from '@@/types/article';
8
+ import type { Category } from '@@/types/category';
9
+ import type { Comment, CreateCommentParams, CommentTargetType } from '@@/types/comment';
10
+ import type { Menu } from '@@/types/theme';
11
+ import type { Notification, GetNotificationsParams } from '@@/types/notification';
12
+ import type { ArchiveItem, SiteStats } from '@@/types/stats';
13
+ import type { Tag } from '@@/types/tag';
14
+
15
+ // ============================================================
16
+ // 文章状态
17
+ // ============================================================
18
+
19
+ export function useCurrentArticle() {
20
+ const currentArticle = useState<Article | null>('currentArticle', () => null);
21
+
22
+ return {
23
+ currentArticle,
24
+ setCurrentArticle: (article: Article | null) => (currentArticle.value = article),
25
+ clearCurrentArticle: () => (currentArticle.value = null),
26
+ };
27
+ }
28
+
29
+ // ============================================================
30
+ // 分类
31
+ // ============================================================
32
+
33
+ export function useCategories() {
34
+ const categories = useState<Category[]>('categories', () => []);
35
+ const total = useState<number>('categories-total', () => 0);
36
+ const loading = ref(false);
37
+ const error = ref<Error | null>(null);
38
+
39
+ const fetchCategories = async (forceRefresh = false) => {
40
+ if (!forceRefresh && categories.value.length) return;
41
+
42
+ loading.value = true;
43
+ error.value = null;
44
+ try {
45
+ const { list, total: resTotal } = await getCategories();
46
+ categories.value = list || [];
47
+ total.value = resTotal || 0;
48
+ } catch (e) {
49
+ console.error('获取分类列表失败:', e);
50
+ error.value = e instanceof Error ? e : new Error(String(e));
51
+ categories.value = [];
52
+ total.value = 0;
53
+ } finally {
54
+ loading.value = false;
55
+ }
56
+ };
57
+
58
+ return { categories, total, loading, error, fetchCategories };
59
+ }
60
+
61
+ // ============================================================
62
+ // 标签
63
+ // ============================================================
64
+
65
+ export function useTags() {
66
+ const tags = useState<Tag[]>('tags', () => []);
67
+ const total = useState<number>('tags-total', () => 0);
68
+ const loading = ref(false);
69
+ const error = ref<Error | null>(null);
70
+
71
+ const fetchTags = async (forceRefresh = false) => {
72
+ if (!forceRefresh && tags.value.length) return;
73
+
74
+ loading.value = true;
75
+ error.value = null;
76
+ try {
77
+ const { list, total: resTotal } = await getTags();
78
+ tags.value = list || [];
79
+ total.value = resTotal || 0;
80
+ } catch (e) {
81
+ console.error('获取标签列表失败:', e);
82
+ error.value = e instanceof Error ? e : new Error(String(e));
83
+ tags.value = [];
84
+ total.value = 0;
85
+ } finally {
86
+ loading.value = false;
87
+ }
88
+ };
89
+
90
+ return { tags, total, loading, error, fetchTags };
91
+ }
92
+
93
+ // ============================================================
94
+ // 菜单
95
+ // ============================================================
96
+
97
+ export function useMenus() {
98
+ const menus = useState<Record<string, Menu[]>>('menus', () => ({}));
99
+
100
+ const filterByKey = (key: string) => (menus.value[key] || []).sort((a, b) => a.sort - b.sort);
101
+
102
+ return {
103
+ menus,
104
+ filterByKey,
105
+ };
106
+ }
107
+
108
+ // ============================================================
109
+ // 评论
110
+ // ============================================================
111
+
112
+ export function useComments() {
113
+ const comments = useState<Comment[]>('comments', () => []);
114
+ const currentTargetType = useState<CommentTargetType | null>('comments-targetType', () => null);
115
+ const currentTargetKey = useState<string | number | null>('comments-targetKey', () => null);
116
+ const { articles } = useArticles();
117
+ const loading = ref(false);
118
+ const error = ref<Error | null>(null);
119
+
120
+ const fetchComments = async (targetType: CommentTargetType, targetKey: string | number) => {
121
+ if (!targetType || !targetKey) return;
122
+
123
+ currentTargetType.value = targetType;
124
+ currentTargetKey.value = targetKey;
125
+
126
+ loading.value = true;
127
+ error.value = null;
128
+ try {
129
+ const data = await getComments({
130
+ target_type: targetType,
131
+ target_key: targetKey,
132
+ });
133
+ comments.value = data.list || [];
134
+ } catch (e) {
135
+ console.error('获取评论失败:', e);
136
+ error.value = e instanceof Error ? e : new Error(String(e));
137
+ comments.value = [];
138
+ } finally {
139
+ loading.value = false;
140
+ }
141
+ };
142
+
143
+ const addComment = async (params: CreateCommentParams) => {
144
+ const newComment = await createComment(params);
145
+
146
+ if (!params.parent_id) {
147
+ comments.value.unshift(newComment);
148
+ } else {
149
+ const addReplyToComment = (commentList: Comment[]): boolean => {
150
+ for (const comment of commentList) {
151
+ if (comment.id === params.parent_id) {
152
+ if (!comment.replies) comment.replies = [];
153
+ comment.replies.push(newComment);
154
+ return true;
155
+ }
156
+ if (comment.replies?.length && addReplyToComment(comment.replies)) return true;
157
+ }
158
+ return false;
159
+ };
160
+ addReplyToComment(comments.value);
161
+ }
162
+
163
+ if (params.target_type === 'article') {
164
+ const article = articles.value.find(a => a.slug === params.target_key);
165
+ if (article) {
166
+ article.comment_count = (article.comment_count || 0) + 1;
167
+ }
168
+ refreshNuxtData('articles-list');
169
+ }
170
+
171
+ return newComment;
172
+ };
173
+
174
+ const removeComment = async (commentId: number) => {
175
+ await deleteComment(commentId);
176
+
177
+ const removeFromList = (commentList: Comment[]): boolean => {
178
+ const index = commentList.findIndex(c => c.id === commentId);
179
+ if (index !== -1) {
180
+ commentList.splice(index, 1);
181
+ return true;
182
+ }
183
+ for (const comment of commentList) {
184
+ if (comment.replies?.length && removeFromList(comment.replies)) return true;
185
+ }
186
+ return false;
187
+ };
188
+ removeFromList(comments.value);
189
+
190
+ if (currentTargetType.value === 'article' && currentTargetKey.value) {
191
+ const article = articles.value.find(a => a.slug === currentTargetKey.value);
192
+ if (article) {
193
+ article.comment_count = Math.max(0, (article.comment_count || 0) - 1);
194
+ }
195
+ refreshNuxtData('articles-list');
196
+ }
197
+ };
198
+
199
+ return {
200
+ comments,
201
+ loading,
202
+ error,
203
+ fetchComments,
204
+ addComment,
205
+ removeComment,
206
+ resetComments: () => {
207
+ comments.value = [];
208
+ currentTargetType.value = null;
209
+ currentTargetKey.value = null;
210
+ },
211
+ flattenComments,
212
+ };
213
+ }
214
+
215
+ // ============================================================
216
+ // 通知
217
+ // ============================================================
218
+
219
+ export function useNotifications() {
220
+ const notifications = useState<Notification[]>('notifications', () => []);
221
+ const total = useState<number>('notifications-total', () => 0);
222
+ const currentPage = useState<number>('notifications-currentPage', () => 1);
223
+ const pageSize = useState<number>('notifications-pageSize', () => 10);
224
+ const unreadCount = useState<number>('notifications-unreadCount', () => 0);
225
+ const loading = ref(false);
226
+ const error = ref<Error | null>(null);
227
+
228
+ const fetchNotifications = async (params?: Partial<GetNotificationsParams>) => {
229
+ loading.value = true;
230
+ error.value = null;
231
+ try {
232
+ const response = await getNotifications({
233
+ page: params?.page ?? currentPage.value,
234
+ page_size: params?.page_size ?? pageSize.value,
235
+ });
236
+ notifications.value = response.list || [];
237
+ total.value = response.total || 0;
238
+ unreadCount.value = response.unread_count || 0;
239
+ if (params?.page) {
240
+ currentPage.value = params.page;
241
+ }
242
+ } catch (e) {
243
+ console.error('获取通知列表失败:', e);
244
+ error.value = e instanceof Error ? e : new Error(String(e));
245
+ notifications.value = [];
246
+ total.value = 0;
247
+ unreadCount.value = 0;
248
+ } finally {
249
+ loading.value = false;
250
+ }
251
+ };
252
+
253
+ const markNotificationAsRead = async (id: number) => {
254
+ try {
255
+ await markAsRead(id);
256
+ const notification = notifications.value.find(n => n.id === id);
257
+ if (notification?.is_read === false) {
258
+ notification.is_read = true;
259
+ notification.read_at = new Date().toISOString();
260
+ unreadCount.value = Math.max(0, unreadCount.value - 1);
261
+ }
262
+ } catch (error) {
263
+ console.error('标记通知已读失败:', error);
264
+ throw error;
265
+ }
266
+ };
267
+
268
+ const markAllNotificationsAsRead = async () => {
269
+ try {
270
+ await markAllAsRead();
271
+ notifications.value.forEach(n => {
272
+ n.is_read = true;
273
+ n.read_at = new Date().toISOString();
274
+ });
275
+ unreadCount.value = 0;
276
+ } catch (error) {
277
+ console.error('标记所有通知已读失败:', error);
278
+ throw error;
279
+ }
280
+ };
281
+
282
+ // 轮询逻辑
283
+ let _pollTimer: ReturnType<typeof setInterval> | null = null;
284
+
285
+ const startPolling = (intervalMs = 30000) => {
286
+ stopPolling();
287
+ _pollTimer = setInterval(() => {
288
+ fetchNotifications({ page: 1, page_size: 1 });
289
+ }, intervalMs);
290
+ };
291
+
292
+ const stopPolling = () => {
293
+ if (_pollTimer) {
294
+ clearInterval(_pollTimer);
295
+ _pollTimer = null;
296
+ }
297
+ };
298
+
299
+ return {
300
+ notifications,
301
+ total,
302
+ currentPage,
303
+ pageSize,
304
+ unreadCount,
305
+ loading,
306
+ error,
307
+ fetchNotifications,
308
+ markNotificationAsRead,
309
+ markAllNotificationsAsRead,
310
+ resetPage: () => (currentPage.value = 1),
311
+ clearNotifications: () => {
312
+ notifications.value = [];
313
+ total.value = 0;
314
+ currentPage.value = 1;
315
+ unreadCount.value = 0;
316
+ },
317
+ startPolling,
318
+ stopPolling,
319
+ };
320
+ }
321
+
322
+ // ============================================================
323
+ // 统计数据
324
+ // ============================================================
325
+
326
+ export function useStats() {
327
+ const siteStats = useState<SiteStats>('siteStats', () => ({
328
+ total_words: '0',
329
+ total_visitors: 0,
330
+ total_page_views: 0,
331
+ online_users: 0,
332
+ total_articles: 0,
333
+ total_comments: 0,
334
+ total_friends: 0,
335
+ total_moments: 0,
336
+ total_categories: 0,
337
+ total_tags: 0,
338
+ today_visitors: 0,
339
+ today_pageviews: 0,
340
+ yesterday_visitors: 0,
341
+ yesterday_pageviews: 0,
342
+ month_pageviews: 0,
343
+ }));
344
+
345
+ const { data } = useAsyncData('site-stats', async () => {
346
+ try {
347
+ return await getSiteStats();
348
+ } catch {
349
+ return null;
350
+ }
351
+ });
352
+
353
+ const stats = computed<SiteStats>(() => ({
354
+ ...siteStats.value,
355
+ ...(data.value ?? {}),
356
+ }));
357
+
358
+ function formatNumber(value: string | number): string {
359
+ if (typeof value === 'string') return value;
360
+ if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
361
+ if (value >= 10000) return `${(value / 10000).toFixed(1)}w`;
362
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}k`;
363
+ return `${value}`;
364
+ }
365
+
366
+ return { siteStats, stats, formatNumber };
367
+ }
368
+
369
+ export function useRunningDays(established?: string) {
370
+ const { basicConfig } = useSysConfig();
371
+ const startDate = computed(() => established || basicConfig.value.established || '2024-01-01');
372
+ const runningDays = computed(() => calcRunningDays(startDate.value));
373
+ return { runningDays };
374
+ }
375
+
376
+ export function useCounts() {
377
+ const {
378
+ data: articlesData,
379
+ pending: articlesPending,
380
+ error: articlesError,
381
+ } = useAsyncData('sidebar-articles-count', async () => {
382
+ try {
383
+ const { total } = await getArticlesForWeb({ page: 1, page_size: 1 });
384
+ return total || 0;
385
+ } catch {
386
+ return 0;
387
+ }
388
+ });
389
+
390
+ const {
391
+ data: categoriesData,
392
+ pending: categoriesPending,
393
+ error: categoriesError,
394
+ } = useAsyncData('sidebar-categories-count', async () => {
395
+ try {
396
+ const { total } = await getCategories();
397
+ return total || 0;
398
+ } catch {
399
+ return 0;
400
+ }
401
+ });
402
+
403
+ const {
404
+ data: tagsData,
405
+ pending: tagsPending,
406
+ error: tagsError,
407
+ } = useAsyncData('sidebar-tags-count', async () => {
408
+ try {
409
+ const { total } = await getTags();
410
+ return total || 0;
411
+ } catch {
412
+ return 0;
413
+ }
414
+ });
415
+
416
+ const articlesTotal = computed(() => articlesData.value ?? 0);
417
+ const categoriesTotal = computed(() => categoriesData.value ?? 0);
418
+ const tagsTotal = computed(() => tagsData.value ?? 0);
419
+
420
+ const loading = computed(
421
+ () => articlesPending.value || categoriesPending.value || tagsPending.value
422
+ );
423
+ const error = computed(() => articlesError.value || categoriesError.value || tagsError.value);
424
+
425
+ return { articlesTotal, categoriesTotal, tagsTotal, loading, error };
426
+ }
427
+
428
+ interface DisplayArchive extends ArchiveItem {
429
+ displayText: string;
430
+ isEarlier: boolean;
431
+ }
432
+
433
+ export async function useArchiveStats() {
434
+ const archives = ref<ArchiveItem[]>([]);
435
+
436
+ const { data, pending, error } = await useAsyncData('archives-stats', async () => {
437
+ try {
438
+ const r = await getArchiveStats();
439
+ return r.archives || [];
440
+ } catch {
441
+ return [];
442
+ }
443
+ });
444
+ if (data.value) {
445
+ archives.value = data.value;
446
+ }
447
+
448
+ const displayArchives = computed<DisplayArchive[]>(() => {
449
+ const list: DisplayArchive[] = archives.value.slice(0, 6).map(a => ({
450
+ ...a,
451
+ displayText: `${a.year} ${a.month}`,
452
+ isEarlier: false,
453
+ }));
454
+ if (archives.value.length > 6) {
455
+ const earlierCount = archives.value.slice(6).reduce((s, a) => s + a.count, 0);
456
+ list.push({
457
+ year: '',
458
+ month: '',
459
+ displayText: '在此之前',
460
+ count: earlierCount,
461
+ isEarlier: true,
462
+ });
463
+ }
464
+ return list;
465
+ });
466
+
467
+ return { archives, displayArchives, loading: pending, error };
468
+ }
469
+
470
+ // ============================================================
471
+ // 系统配置
472
+ // ============================================================
473
+
474
+ export function useSysConfig() {
475
+ const basicConfig = useState<Record<string, string>>('sysconfig-basic', () => ({
476
+ author: '',
477
+ author_desc: '',
478
+ author_avatar: '',
479
+ icp: '',
480
+ police_record: '',
481
+ admin_url: '',
482
+ blog_url: '',
483
+ home_url: '',
484
+ title: 'FlecBLOG',
485
+ subtitle: 'FlecBLOG',
486
+ description: '',
487
+ keywords: '',
488
+ established: '',
489
+ favicon: '',
490
+ emojis: '',
491
+ custom_head: '',
492
+ custom_body: '',
493
+ meting_api: '',
494
+ cravatar_url: '',
495
+ ip_api_url: '',
496
+ cover_maker_api: '',
497
+ }));
498
+
499
+ const oauthConfig = useState<Record<string, string>>('sysconfig-oauth', () => ({
500
+ 'github.enabled': 'false',
501
+ 'google.enabled': 'false',
502
+ 'qq.enabled': 'false',
503
+ 'microsoft.enabled': 'false',
504
+ 'oidc.enabled': 'false',
505
+ 'wechat.enabled': 'false',
506
+ }));
507
+
508
+ const uploadConfig = useState<Record<string, string>>('sysconfig-upload', () => ({
509
+ max_file_size: '5',
510
+ }));
511
+
512
+ function getString(key: string, fallback: string = ''): string {
513
+ const val = basicConfig.value[key];
514
+ return val !== undefined && val !== '' ? val : fallback;
515
+ }
516
+
517
+ function getNumber(key: string, fallback: number = 0): number {
518
+ const val = basicConfig.value[key];
519
+ if (val === undefined || val === '') return fallback;
520
+ const parsed = Number(val);
521
+ return isNaN(parsed) ? fallback : parsed;
522
+ }
523
+
524
+ function getBoolean(key: string, fallback: boolean = false): boolean {
525
+ const val = basicConfig.value[key];
526
+ if (val === undefined || val === '') return fallback;
527
+ return val === 'true';
528
+ }
529
+
530
+ function getArray<T = unknown>(key: string, fallback: T[] = []): T[] {
531
+ const val = basicConfig.value[key];
532
+ if (!val) return fallback;
533
+ try {
534
+ const parsed = JSON.parse(val);
535
+ return Array.isArray(parsed) ? (parsed as T[]) : fallback;
536
+ } catch {
537
+ return fallback;
538
+ }
539
+ }
540
+
541
+ function getOAuthBoolean(key: string, fallback: boolean = false): boolean {
542
+ const val = oauthConfig.value[key];
543
+ if (val === undefined || val === '') return fallback;
544
+ return val === 'true';
545
+ }
546
+
547
+ return {
548
+ basicConfig,
549
+ oauthConfig,
550
+ uploadConfig,
551
+ getString,
552
+ getNumber,
553
+ getBoolean,
554
+ getArray,
555
+ getOAuthBoolean,
556
+ };
557
+ }
@@ -0,0 +1,13 @@
1
+ import { subscribe as subscribeApi, unsubscribe as unsubscribeApi } from './api/subscribe';
2
+
3
+ export function useSubscribe() {
4
+ const subscribe = async (email: string) => {
5
+ return subscribeApi(email);
6
+ };
7
+
8
+ const unsubscribe = async (token: string) => {
9
+ return unsubscribeApi(token);
10
+ };
11
+
12
+ return { subscribe, unsubscribe };
13
+ }
@@ -0,0 +1,61 @@
1
+ import type { ThemeState } from '@@/types/theme';
2
+ import { getActiveThemeSchema } from '@/composables/api/theme';
3
+
4
+ export function useTheme() {
5
+ const theme = useState<ThemeState>('active-theme', () => ({
6
+ slug: 'default',
7
+ name: '默认主题',
8
+ schema: {},
9
+ config: {},
10
+ loaded: false,
11
+ }));
12
+
13
+ const config = computed(() => theme.value.config as Record<string, unknown>);
14
+
15
+ function getString(key: string, fallback: string = ''): string {
16
+ const val = config.value[key];
17
+ return typeof val === 'string' && val !== '' ? val : fallback;
18
+ }
19
+
20
+ function getNumber(key: string, fallback: number = 0): number {
21
+ const val = config.value[key];
22
+ return typeof val === 'number' ? val : fallback;
23
+ }
24
+
25
+ function getBoolean(key: string, fallback: boolean = false): boolean {
26
+ const val = config.value[key];
27
+ return typeof val === 'boolean' ? val : fallback;
28
+ }
29
+
30
+ function getArray<T = unknown>(key: string, fallback: T[] = []): T[] {
31
+ const val = config.value[key];
32
+ return Array.isArray(val) ? (val as T[]) : fallback;
33
+ }
34
+
35
+ const fetchTheme = async () => {
36
+ if (theme.value.loaded) return;
37
+
38
+ try {
39
+ const data = await getActiveThemeSchema();
40
+ theme.value.slug = data.slug;
41
+ theme.value.name = data.name || data.slug;
42
+ theme.value.schema = data.schema;
43
+ theme.value.config = data.config ?? {};
44
+ theme.value.loaded = true;
45
+ } catch {
46
+ theme.value.slug = 'default';
47
+ theme.value.name = '默认主题';
48
+ theme.value.loaded = true;
49
+ }
50
+ };
51
+
52
+ return {
53
+ theme,
54
+ config,
55
+ getString,
56
+ getNumber,
57
+ getBoolean,
58
+ getArray,
59
+ fetchTheme,
60
+ };
61
+ }