Spaces:
Runtime error
Runtime error
Commit
•
dbc8f44
1
Parent(s):
c985fd8
up
Browse files- package-lock.json +45 -0
- package.json +1 -0
- src/app/interface/bottom-bar/index.tsx +27 -0
- src/app/interface/grid/index.tsx +1 -1
- src/app/interface/zoom/index.tsx +4 -1
- src/app/main.tsx +13 -2
- src/app/store/index.ts +31 -0
package-lock.json
CHANGED
@@ -39,6 +39,7 @@
|
|
39 |
"date-fns": "^2.30.0",
|
40 |
"eslint": "8.45.0",
|
41 |
"eslint-config-next": "13.4.10",
|
|
|
42 |
"lucide-react": "^0.260.0",
|
43 |
"next": "13.4.10",
|
44 |
"pick": "^0.0.1",
|
@@ -4200,6 +4201,14 @@
|
|
4200 |
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
4201 |
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
4202 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4203 |
"node_modules/base64-js": {
|
4204 |
"version": "1.5.1",
|
4205 |
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
@@ -4820,6 +4829,14 @@
|
|
4820 |
"node": ">=4"
|
4821 |
}
|
4822 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4823 |
"node_modules/css-to-react-native": {
|
4824 |
"version": "3.2.0",
|
4825 |
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
@@ -6026,6 +6043,18 @@
|
|
6026 |
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
|
6027 |
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="
|
6028 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6029 |
"node_modules/htmlparser2": {
|
6030 |
"version": "8.0.2",
|
6031 |
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
@@ -8109,6 +8138,14 @@
|
|
8109 |
"node": ">=6"
|
8110 |
}
|
8111 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8112 |
"node_modules/text-table": {
|
8113 |
"version": "0.2.0",
|
8114 |
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
@@ -8532,6 +8569,14 @@
|
|
8532 |
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
8533 |
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
8534 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8535 |
"node_modules/uuid": {
|
8536 |
"version": "9.0.0",
|
8537 |
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
|
|
39 |
"date-fns": "^2.30.0",
|
40 |
"eslint": "8.45.0",
|
41 |
"eslint-config-next": "13.4.10",
|
42 |
+
"html2canvas": "^1.4.1",
|
43 |
"lucide-react": "^0.260.0",
|
44 |
"next": "13.4.10",
|
45 |
"pick": "^0.0.1",
|
|
|
4201 |
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
4202 |
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
4203 |
},
|
4204 |
+
"node_modules/base64-arraybuffer": {
|
4205 |
+
"version": "1.0.2",
|
4206 |
+
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
4207 |
+
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
4208 |
+
"engines": {
|
4209 |
+
"node": ">= 0.6.0"
|
4210 |
+
}
|
4211 |
+
},
|
4212 |
"node_modules/base64-js": {
|
4213 |
"version": "1.5.1",
|
4214 |
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
|
|
4829 |
"node": ">=4"
|
4830 |
}
|
4831 |
},
|
4832 |
+
"node_modules/css-line-break": {
|
4833 |
+
"version": "2.1.0",
|
4834 |
+
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
4835 |
+
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
4836 |
+
"dependencies": {
|
4837 |
+
"utrie": "^1.0.2"
|
4838 |
+
}
|
4839 |
+
},
|
4840 |
"node_modules/css-to-react-native": {
|
4841 |
"version": "3.2.0",
|
4842 |
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
|
|
6043 |
"resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz",
|
6044 |
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg=="
|
6045 |
},
|
6046 |
+
"node_modules/html2canvas": {
|
6047 |
+
"version": "1.4.1",
|
6048 |
+
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
6049 |
+
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
6050 |
+
"dependencies": {
|
6051 |
+
"css-line-break": "^2.1.0",
|
6052 |
+
"text-segmentation": "^1.0.3"
|
6053 |
+
},
|
6054 |
+
"engines": {
|
6055 |
+
"node": ">=8.0.0"
|
6056 |
+
}
|
6057 |
+
},
|
6058 |
"node_modules/htmlparser2": {
|
6059 |
"version": "8.0.2",
|
6060 |
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
|
|
8138 |
"node": ">=6"
|
8139 |
}
|
8140 |
},
|
8141 |
+
"node_modules/text-segmentation": {
|
8142 |
+
"version": "1.0.3",
|
8143 |
+
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
8144 |
+
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
8145 |
+
"dependencies": {
|
8146 |
+
"utrie": "^1.0.2"
|
8147 |
+
}
|
8148 |
+
},
|
8149 |
"node_modules/text-table": {
|
8150 |
"version": "0.2.0",
|
8151 |
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
|
|
8569 |
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
8570 |
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
8571 |
},
|
8572 |
+
"node_modules/utrie": {
|
8573 |
+
"version": "1.0.2",
|
8574 |
+
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
8575 |
+
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
8576 |
+
"dependencies": {
|
8577 |
+
"base64-arraybuffer": "^1.0.2"
|
8578 |
+
}
|
8579 |
+
},
|
8580 |
"node_modules/uuid": {
|
8581 |
"version": "9.0.0",
|
8582 |
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
package.json
CHANGED
@@ -40,6 +40,7 @@
|
|
40 |
"date-fns": "^2.30.0",
|
41 |
"eslint": "8.45.0",
|
42 |
"eslint-config-next": "13.4.10",
|
|
|
43 |
"lucide-react": "^0.260.0",
|
44 |
"next": "13.4.10",
|
45 |
"pick": "^0.0.1",
|
|
|
40 |
"date-fns": "^2.30.0",
|
41 |
"eslint": "8.45.0",
|
42 |
"eslint-config-next": "13.4.10",
|
43 |
+
"html2canvas": "^1.4.1",
|
44 |
"lucide-react": "^0.260.0",
|
45 |
"next": "13.4.10",
|
46 |
"pick": "^0.0.1",
|
src/app/interface/bottom-bar/index.tsx
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useStore } from "@/app/store"
|
2 |
+
import { Button } from "@/components/ui/button"
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function BottomBar() {
|
6 |
+
const download = useStore(state => state.download)
|
7 |
+
const isGeneratingStory = useStore(state => state.isGeneratingStory)
|
8 |
+
const prompt = useStore(state => state.prompt)
|
9 |
+
const panelGenerationStatus = useStore(state => state.panelGenerationStatus)
|
10 |
+
|
11 |
+
const remainingImages = Object.values(panelGenerationStatus).reduce((acc, s) => (acc + (s ? 1 : 0)), 0)
|
12 |
+
|
13 |
+
return (
|
14 |
+
<div className={cn(
|
15 |
+
`fixed bottom-8 right-8`,
|
16 |
+
`flex flex-row`,
|
17 |
+
`animation-all duration-300 ease-in-out`,
|
18 |
+
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
19 |
+
)}>
|
20 |
+
<div>
|
21 |
+
<Button onClick={download} disabled={!prompt?.length}>{
|
22 |
+
remainingImages ? `${remainingImages} remaining..` : `Download`
|
23 |
+
}</Button>
|
24 |
+
</div>
|
25 |
+
</div>
|
26 |
+
)
|
27 |
+
}
|
src/app/interface/grid/index.tsx
CHANGED
@@ -7,6 +7,7 @@ import { useStore } from "@/app/store"
|
|
7 |
|
8 |
export function Grid({ children, className }: { children: ReactNode; className: string }) {
|
9 |
const zoomLevel = useStore(state => state.zoomLevel)
|
|
|
10 |
return (
|
11 |
<div
|
12 |
// the "fixed" width ensure our comic keeps a consistent ratio
|
@@ -23,4 +24,3 @@ export function Grid({ children, className }: { children: ReactNode; className:
|
|
23 |
)
|
24 |
}
|
25 |
|
26 |
-
|
|
|
7 |
|
8 |
export function Grid({ children, className }: { children: ReactNode; className: string }) {
|
9 |
const zoomLevel = useStore(state => state.zoomLevel)
|
10 |
+
|
11 |
return (
|
12 |
<div
|
13 |
// the "fixed" width ensure our comic keeps a consistent ratio
|
|
|
24 |
)
|
25 |
}
|
26 |
|
|
src/app/interface/zoom/index.tsx
CHANGED
@@ -5,11 +5,14 @@ import { cn } from "@/lib/utils"
|
|
5 |
export function Zoom() {
|
6 |
const zoomLevel = useStore((state) => state.zoomLevel)
|
7 |
const setZoomLevel = useStore((state) => state.setZoomLevel)
|
|
|
8 |
|
9 |
return (
|
10 |
<div className={cn(
|
11 |
// `fixed flex items-center justify-center bottom-8 top-32 right-8 z-10 h-screen`,
|
12 |
-
`fixed flex flex-col items-center bottom-8 top-32 md:top-20 right-6 z-10
|
|
|
|
|
13 |
)}>
|
14 |
<div className="font-mono text-xs pb-1 text-stone-700">
|
15 |
Zoom
|
|
|
5 |
export function Zoom() {
|
6 |
const zoomLevel = useStore((state) => state.zoomLevel)
|
7 |
const setZoomLevel = useStore((state) => state.setZoomLevel)
|
8 |
+
const isGeneratingStory = useStore((state) => state.isGeneratingStory)
|
9 |
|
10 |
return (
|
11 |
<div className={cn(
|
12 |
// `fixed flex items-center justify-center bottom-8 top-32 right-8 z-10 h-screen`,
|
13 |
+
`fixed flex flex-col items-center bottom-8 top-32 md:top-20 right-6 z-10`,
|
14 |
+
`animation-all duration-300 ease-in-out`,
|
15 |
+
isGeneratingStory ? `scale-0 opacity-0` : ``,
|
16 |
)}>
|
17 |
<div className="font-mono text-xs pb-1 text-stone-700">
|
18 |
Zoom
|
src/app/main.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useEffect, useTransition } from "react"
|
4 |
import { useSearchParams } from "next/navigation"
|
5 |
|
6 |
import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
|
@@ -12,6 +12,7 @@ import { getRandomLayoutName, layouts } from "./layouts"
|
|
12 |
import { useStore } from "./store"
|
13 |
import { Zoom } from "./interface/zoom"
|
14 |
import { getStory } from "./queries/getStory"
|
|
|
15 |
|
16 |
export default function Main() {
|
17 |
const [_isPending, startTransition] = useTransition()
|
@@ -40,6 +41,15 @@ export default function Main() {
|
|
40 |
|
41 |
const zoomLevel = useStore(state => state.zoomLevel)
|
42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
// react to URL params
|
44 |
useEffect(() => {
|
45 |
if (requestedPreset && requestedPreset !== preset.label) { setPreset(getPreset(requestedPreset)) }
|
@@ -103,7 +113,7 @@ export default function Main() {
|
|
103 |
)}>
|
104 |
<div className="flex flex-col items-center w-full">
|
105 |
<div
|
106 |
-
|
107 |
className={cn(
|
108 |
`flex flex-col items-center justify-start`,
|
109 |
|
@@ -126,6 +136,7 @@ export default function Main() {
|
|
126 |
</div>
|
127 |
</div>
|
128 |
<Zoom />
|
|
|
129 |
<div className={cn(
|
130 |
`z-20 fixed inset-0`,
|
131 |
`flex flex-row items-center justify-center`,
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { useEffect, useRef, useTransition } from "react"
|
4 |
import { useSearchParams } from "next/navigation"
|
5 |
|
6 |
import { PresetName, defaultPreset, getPreset } from "@/app/engine/presets"
|
|
|
12 |
import { useStore } from "./store"
|
13 |
import { Zoom } from "./interface/zoom"
|
14 |
import { getStory } from "./queries/getStory"
|
15 |
+
import { BottomBar } from "./interface/bottom-bar"
|
16 |
|
17 |
export default function Main() {
|
18 |
const [_isPending, startTransition] = useTransition()
|
|
|
41 |
|
42 |
const zoomLevel = useStore(state => state.zoomLevel)
|
43 |
|
44 |
+
const setPage = useStore(state => state.setPage)
|
45 |
+
const pageRef = useRef<HTMLDivElement>(null)
|
46 |
+
|
47 |
+
useEffect(() => {
|
48 |
+
const element = pageRef.current
|
49 |
+
if (!element) { return }
|
50 |
+
setPage(element)
|
51 |
+
}, [pageRef.current])
|
52 |
+
|
53 |
// react to URL params
|
54 |
useEffect(() => {
|
55 |
if (requestedPreset && requestedPreset !== preset.label) { setPreset(getPreset(requestedPreset)) }
|
|
|
113 |
)}>
|
114 |
<div className="flex flex-col items-center w-full">
|
115 |
<div
|
116 |
+
ref={pageRef}
|
117 |
className={cn(
|
118 |
`flex flex-col items-center justify-start`,
|
119 |
|
|
|
136 |
</div>
|
137 |
</div>
|
138 |
<Zoom />
|
139 |
+
<BottomBar />
|
140 |
<div className={cn(
|
141 |
`z-20 fixed inset-0`,
|
142 |
`flex flex-row items-center justify-center`,
|
src/app/store/index.ts
CHANGED
@@ -5,6 +5,7 @@ import { create } from "zustand"
|
|
5 |
import { FontName } from "@/lib/fonts"
|
6 |
import { Preset, getPreset } from "@/app/engine/presets"
|
7 |
import { LayoutName, getRandomLayoutName } from "../layouts"
|
|
|
8 |
|
9 |
export const useStore = create<{
|
10 |
prompt: string
|
@@ -14,6 +15,7 @@ export const useStore = create<{
|
|
14 |
captions: Record<string, string>
|
15 |
layout: LayoutName
|
16 |
zoomLevel: number
|
|
|
17 |
isGeneratingStory: boolean
|
18 |
panelGenerationStatus: Record<number, boolean>
|
19 |
isGeneratingText: boolean
|
@@ -25,9 +27,11 @@ export const useStore = create<{
|
|
25 |
setLayout: (layout: LayoutName) => void
|
26 |
setCaption: (panelId: number, caption: string) => void
|
27 |
setZoomLevel: (zoomLevel: number) => void
|
|
|
28 |
setGeneratingStory: (isGeneratingStory: boolean) => void
|
29 |
setGeneratingImages: (panelId: number, value: boolean) => void
|
30 |
setGeneratingText: (isGeneratingText: boolean) => void
|
|
|
31 |
}>((set, get) => ({
|
32 |
prompt: "",
|
33 |
font: "actionman",
|
@@ -36,6 +40,7 @@ export const useStore = create<{
|
|
36 |
captions: {},
|
37 |
layout: getRandomLayoutName(),
|
38 |
zoomLevel: 50,
|
|
|
39 |
isGeneratingStory: false,
|
40 |
panelGenerationStatus: {},
|
41 |
isGeneratingText: false,
|
@@ -81,6 +86,10 @@ export const useStore = create<{
|
|
81 |
},
|
82 |
setLayout: (layout: LayoutName) => set({ layout }),
|
83 |
setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
|
|
|
|
|
|
|
|
|
84 |
setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
|
85 |
setGeneratingImages: (panelId: number, value: boolean) => {
|
86 |
|
@@ -97,4 +106,26 @@ export const useStore = create<{
|
|
97 |
})
|
98 |
},
|
99 |
setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
}))
|
|
|
5 |
import { FontName } from "@/lib/fonts"
|
6 |
import { Preset, getPreset } from "@/app/engine/presets"
|
7 |
import { LayoutName, getRandomLayoutName } from "../layouts"
|
8 |
+
import html2canvas from "html2canvas"
|
9 |
|
10 |
export const useStore = create<{
|
11 |
prompt: string
|
|
|
15 |
captions: Record<string, string>
|
16 |
layout: LayoutName
|
17 |
zoomLevel: number
|
18 |
+
page: HTMLDivElement
|
19 |
isGeneratingStory: boolean
|
20 |
panelGenerationStatus: Record<number, boolean>
|
21 |
isGeneratingText: boolean
|
|
|
27 |
setLayout: (layout: LayoutName) => void
|
28 |
setCaption: (panelId: number, caption: string) => void
|
29 |
setZoomLevel: (zoomLevel: number) => void
|
30 |
+
setPage: (page: HTMLDivElement) => void
|
31 |
setGeneratingStory: (isGeneratingStory: boolean) => void
|
32 |
setGeneratingImages: (panelId: number, value: boolean) => void
|
33 |
setGeneratingText: (isGeneratingText: boolean) => void
|
34 |
+
download: () => void
|
35 |
}>((set, get) => ({
|
36 |
prompt: "",
|
37 |
font: "actionman",
|
|
|
40 |
captions: {},
|
41 |
layout: getRandomLayoutName(),
|
42 |
zoomLevel: 50,
|
43 |
+
page: undefined as unknown as HTMLDivElement,
|
44 |
isGeneratingStory: false,
|
45 |
panelGenerationStatus: {},
|
46 |
isGeneratingText: false,
|
|
|
86 |
},
|
87 |
setLayout: (layout: LayoutName) => set({ layout }),
|
88 |
setZoomLevel: (zoomLevel: number) => set({ zoomLevel }),
|
89 |
+
setPage: (page: HTMLDivElement) => {
|
90 |
+
if (!page) { return }
|
91 |
+
set({ page })
|
92 |
+
},
|
93 |
setGeneratingStory: (isGeneratingStory: boolean) => set({ isGeneratingStory }),
|
94 |
setGeneratingImages: (panelId: number, value: boolean) => {
|
95 |
|
|
|
106 |
})
|
107 |
},
|
108 |
setGeneratingText: (isGeneratingText: boolean) => set({ isGeneratingText }),
|
109 |
+
download: async () => {
|
110 |
+
console.log("download called!")
|
111 |
+
const { page } = get()
|
112 |
+
console.log("page:", page)
|
113 |
+
if (!page) { return }
|
114 |
+
|
115 |
+
const canvas = await html2canvas(page)
|
116 |
+
console.log("canvas:", canvas)
|
117 |
+
|
118 |
+
const data = canvas.toDataURL('image/jpg')
|
119 |
+
const link = document.createElement('a')
|
120 |
+
|
121 |
+
if (typeof link.download === 'string') {
|
122 |
+
link.href = data
|
123 |
+
link.download = 'comic.jpg'
|
124 |
+
document.body.appendChild(link)
|
125 |
+
link.click()
|
126 |
+
document.body.removeChild(link)
|
127 |
+
} else {
|
128 |
+
window.open(data)
|
129 |
+
}
|
130 |
+
}
|
131 |
}))
|