@muhammedaksam/easiarr 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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/package.json +72 -0
- package/src/VersionInfo.ts +12 -0
- package/src/api/arr-api.ts +198 -0
- package/src/api/index.ts +1 -0
- package/src/apps/categories.ts +14 -0
- package/src/apps/index.ts +2 -0
- package/src/apps/registry.ts +868 -0
- package/src/compose/generator.ts +234 -0
- package/src/compose/index.ts +2 -0
- package/src/compose/templates.ts +68 -0
- package/src/config/defaults.ts +37 -0
- package/src/config/index.ts +3 -0
- package/src/config/manager.ts +109 -0
- package/src/config/schema.ts +191 -0
- package/src/docker/client.ts +129 -0
- package/src/docker/index.ts +1 -0
- package/src/index.ts +24 -0
- package/src/structure/manager.ts +86 -0
- package/src/ui/App.ts +95 -0
- package/src/ui/components/ApplicationSelector.ts +256 -0
- package/src/ui/components/FileEditor.ts +91 -0
- package/src/ui/components/PageLayout.ts +104 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/screens/AdvancedSettings.ts +177 -0
- package/src/ui/screens/ApiKeyViewer.ts +223 -0
- package/src/ui/screens/AppConfigurator.ts +549 -0
- package/src/ui/screens/AppManager.ts +271 -0
- package/src/ui/screens/ContainerControl.ts +142 -0
- package/src/ui/screens/MainMenu.ts +161 -0
- package/src/ui/screens/QuickSetup.ts +1110 -0
- package/src/ui/screens/SecretsEditor.ts +256 -0
- package/src/ui/screens/index.ts +4 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Manager Screen
|
|
3
|
+
* Add, remove, or configure apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RenderContext, CliRenderer, KeyEvent } from "@opentui/core"
|
|
7
|
+
import { BoxRenderable, TextRenderable, SelectRenderable, SelectRenderableEvents, RGBA } from "@opentui/core"
|
|
8
|
+
import type { App } from "../App"
|
|
9
|
+
import type { AppId, EasiarrConfig } from "../../config/schema"
|
|
10
|
+
import { saveConfig } from "../../config"
|
|
11
|
+
import { saveCompose } from "../../compose"
|
|
12
|
+
import { createPageLayout } from "../components/PageLayout"
|
|
13
|
+
import { ensureDirectoryStructure } from "../../structure/manager"
|
|
14
|
+
import { ApplicationSelector } from "../components/ApplicationSelector"
|
|
15
|
+
import { getApp } from "../../apps/registry"
|
|
16
|
+
import { SecretsEditor } from "./SecretsEditor"
|
|
17
|
+
|
|
18
|
+
export class AppManager {
|
|
19
|
+
private renderer: RenderContext
|
|
20
|
+
private container: BoxRenderable
|
|
21
|
+
private app: App
|
|
22
|
+
private config: EasiarrConfig
|
|
23
|
+
private selector: ApplicationSelector | null = null
|
|
24
|
+
private navMenu: SelectRenderable | null = null
|
|
25
|
+
private keyHandler: ((key: KeyEvent) => void) | null = null
|
|
26
|
+
private activeZone: "selector" | "nav" = "selector"
|
|
27
|
+
private previouslyEnabledApps: Set<AppId>
|
|
28
|
+
private page: BoxRenderable | null = null
|
|
29
|
+
|
|
30
|
+
constructor(renderer: RenderContext, container: BoxRenderable, app: App, config: EasiarrConfig) {
|
|
31
|
+
this.renderer = renderer
|
|
32
|
+
this.container = container
|
|
33
|
+
this.app = app
|
|
34
|
+
this.config = config
|
|
35
|
+
// Track which apps were enabled before user starts editing
|
|
36
|
+
this.previouslyEnabledApps = new Set(config.apps.filter((a) => a.enabled).map((a) => a.id))
|
|
37
|
+
|
|
38
|
+
this.render()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private render(): void {
|
|
42
|
+
// Clean up previous listeners
|
|
43
|
+
if (this.keyHandler && (this.renderer as CliRenderer).keyInput) {
|
|
44
|
+
;(this.renderer as CliRenderer).keyInput.off("keypress", this.keyHandler)
|
|
45
|
+
this.keyHandler = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Clear container
|
|
49
|
+
const children = this.container.getChildren()
|
|
50
|
+
for (const child of children) {
|
|
51
|
+
this.container.remove(child.id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
|
|
55
|
+
title: "Manage Apps",
|
|
56
|
+
stepInfo: "Toggle apps linked to your configuration",
|
|
57
|
+
footerHint: "←→ Tab Enter Toggle s Save q Back",
|
|
58
|
+
})
|
|
59
|
+
this.page = page
|
|
60
|
+
|
|
61
|
+
// Selected Apps Set for the selector
|
|
62
|
+
// Selected Apps Set for the selector
|
|
63
|
+
// We create a temporary Set to track changes, then commit to config on "Save".
|
|
64
|
+
// Changes are modified in memory immediately to match QuickSetup behavior.
|
|
65
|
+
|
|
66
|
+
const enabledApps = new Set(this.config.apps.filter((a) => a.enabled).map((a) => a.id))
|
|
67
|
+
|
|
68
|
+
this.selector = new ApplicationSelector(this.renderer as CliRenderer, {
|
|
69
|
+
selectedApps: enabledApps,
|
|
70
|
+
width: "100%",
|
|
71
|
+
flexGrow: 1, // list takes available space
|
|
72
|
+
onToggle: (appId, enabled) => {
|
|
73
|
+
this.toggleApp(appId, enabled)
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
content.add(this.selector)
|
|
77
|
+
|
|
78
|
+
// Separator
|
|
79
|
+
content.add(new TextRenderable(this.renderer, { content: " " }))
|
|
80
|
+
|
|
81
|
+
// Nav Menu (Save / Back)
|
|
82
|
+
this.navMenu = new SelectRenderable(this.renderer, {
|
|
83
|
+
id: "app-manager-nav",
|
|
84
|
+
width: "100%",
|
|
85
|
+
height: 4,
|
|
86
|
+
options: [
|
|
87
|
+
{ name: "💾 Save & Apply", description: "Write config and regenerate docker-compose.yml" },
|
|
88
|
+
{ name: "❌ Discard / Back", description: "Return to Main Menu" },
|
|
89
|
+
],
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
this.navMenu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
93
|
+
if (index === 0) {
|
|
94
|
+
await this.save()
|
|
95
|
+
} else {
|
|
96
|
+
this.app.navigateTo("main")
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
content.add(this.navMenu)
|
|
101
|
+
|
|
102
|
+
this.container.add(page)
|
|
103
|
+
|
|
104
|
+
// Initial Focus
|
|
105
|
+
this.selector.focus()
|
|
106
|
+
this.activeZone = "selector"
|
|
107
|
+
|
|
108
|
+
// Key Handler
|
|
109
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
110
|
+
if (key.name === "q") {
|
|
111
|
+
this.app.navigateTo("main")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (key.name === "s" && key.ctrl) {
|
|
116
|
+
// Ctrl+S to save
|
|
117
|
+
this.save()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (key.name === "tab") {
|
|
122
|
+
// Switch zones
|
|
123
|
+
if (this.activeZone === "selector") {
|
|
124
|
+
this.activeZone = "nav"
|
|
125
|
+
this.selector?.blur()
|
|
126
|
+
this.navMenu?.focus()
|
|
127
|
+
} else {
|
|
128
|
+
this.activeZone = "selector"
|
|
129
|
+
this.navMenu?.blur()
|
|
130
|
+
this.selector?.focus()
|
|
131
|
+
}
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Delegate to selector if active
|
|
136
|
+
if (this.activeZone === "selector" && this.selector) {
|
|
137
|
+
const handled = this.selector.handleKey(key)
|
|
138
|
+
if (handled) return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
;(this.renderer as CliRenderer).keyInput.on("keypress", this.keyHandler)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private toggleApp(id: AppId, enabled: boolean): void {
|
|
145
|
+
const existingIndex = this.config.apps.findIndex((a) => a.id === id)
|
|
146
|
+
if (existingIndex >= 0) {
|
|
147
|
+
this.config.apps[existingIndex].enabled = enabled
|
|
148
|
+
} else {
|
|
149
|
+
if (enabled) {
|
|
150
|
+
this.config.apps.push({ id, enabled: true })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Auto-enable Traefik config when traefik app is enabled
|
|
155
|
+
if (id === "traefik") {
|
|
156
|
+
if (enabled && !this.config.traefik?.enabled) {
|
|
157
|
+
this.config.traefik = {
|
|
158
|
+
enabled: true,
|
|
159
|
+
domain: "${CLOUDFLARE_DNS_ZONE}",
|
|
160
|
+
entrypoint: "websecure",
|
|
161
|
+
middlewares: [],
|
|
162
|
+
}
|
|
163
|
+
} else if (!enabled && this.config.traefik?.enabled) {
|
|
164
|
+
this.config.traefik.enabled = false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Auto-enable VPN config when gluetun app is enabled
|
|
169
|
+
if (id === "gluetun") {
|
|
170
|
+
if (enabled && !this.config.vpn) {
|
|
171
|
+
this.config.vpn = { mode: "mini" }
|
|
172
|
+
} else if (!enabled && this.config.vpn) {
|
|
173
|
+
this.config.vpn.mode = "none"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async save() {
|
|
179
|
+
await saveConfig(this.config)
|
|
180
|
+
await ensureDirectoryStructure(this.config)
|
|
181
|
+
await saveCompose(this.config)
|
|
182
|
+
|
|
183
|
+
// Check for newly-enabled apps that have secrets
|
|
184
|
+
const currentlyEnabled = new Set(this.config.apps.filter((a) => a.enabled).map((a) => a.id))
|
|
185
|
+
const newlyEnabled = [...currentlyEnabled].filter((id) => !this.previouslyEnabledApps.has(id))
|
|
186
|
+
const appsWithSecrets = newlyEnabled.filter((id) => {
|
|
187
|
+
const appDef = getApp(id)
|
|
188
|
+
return appDef?.secrets && appDef.secrets.length > 0
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
if (appsWithSecrets.length > 0) {
|
|
192
|
+
this.showSecretsPrompt(appsWithSecrets)
|
|
193
|
+
} else {
|
|
194
|
+
this.app.navigateTo("main")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private showSecretsPrompt(appsWithSecrets: AppId[]) {
|
|
199
|
+
// Hide the main page
|
|
200
|
+
if (this.page) this.page.visible = false
|
|
201
|
+
if (this.keyHandler) {
|
|
202
|
+
;(this.renderer as CliRenderer).keyInput.off("keypress", this.keyHandler)
|
|
203
|
+
this.keyHandler = null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create a prompt overlay
|
|
207
|
+
const overlay = new BoxRenderable(this.renderer, {
|
|
208
|
+
id: "secrets-prompt-overlay",
|
|
209
|
+
width: "100%",
|
|
210
|
+
height: "100%",
|
|
211
|
+
backgroundColor: RGBA.fromHex("#111111"),
|
|
212
|
+
zIndex: 200,
|
|
213
|
+
flexDirection: "column",
|
|
214
|
+
padding: 2,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const appNames = appsWithSecrets.map((id) => getApp(id)?.name || id).join(", ")
|
|
218
|
+
overlay.add(
|
|
219
|
+
new TextRenderable(this.renderer, {
|
|
220
|
+
content: "🔑 New Apps Require Configuration",
|
|
221
|
+
fg: "#f1fa8c",
|
|
222
|
+
marginBottom: 1,
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
overlay.add(
|
|
226
|
+
new TextRenderable(this.renderer, {
|
|
227
|
+
content: `The following apps need secrets configured: ${appNames}`,
|
|
228
|
+
fg: "#aaaaaa",
|
|
229
|
+
marginBottom: 2,
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const menu = new SelectRenderable(this.renderer, {
|
|
234
|
+
id: "secrets-prompt-menu",
|
|
235
|
+
width: "100%",
|
|
236
|
+
height: 4,
|
|
237
|
+
options: [
|
|
238
|
+
{ name: "✓ Configure Secrets Now", description: "Open the Secrets Editor" },
|
|
239
|
+
{ name: "✗ Skip for Now", description: "Return to Main Menu" },
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
menu.on(SelectRenderableEvents.ITEM_SELECTED, (index) => {
|
|
244
|
+
this.container.remove("secrets-prompt-overlay")
|
|
245
|
+
if (index === 0) {
|
|
246
|
+
// Show SecretsEditor
|
|
247
|
+
const editor = new SecretsEditor(this.renderer as CliRenderer, {
|
|
248
|
+
id: "secrets-editor-overlay",
|
|
249
|
+
width: "100%",
|
|
250
|
+
height: "100%",
|
|
251
|
+
config: this.config,
|
|
252
|
+
onSave: () => {
|
|
253
|
+
this.container.remove("secrets-editor-overlay")
|
|
254
|
+
this.app.navigateTo("main")
|
|
255
|
+
},
|
|
256
|
+
onCancel: () => {
|
|
257
|
+
this.container.remove("secrets-editor-overlay")
|
|
258
|
+
this.app.navigateTo("main")
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
this.container.add(editor)
|
|
262
|
+
} else {
|
|
263
|
+
this.app.navigateTo("main")
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
overlay.add(menu)
|
|
268
|
+
this.container.add(overlay)
|
|
269
|
+
menu.focus()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container Control Screen
|
|
3
|
+
* Start, stop, restart containers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RenderContext, CliRenderer } from "@opentui/core"
|
|
7
|
+
import { BoxRenderable, TextRenderable, SelectRenderable, SelectRenderableEvents } from "@opentui/core"
|
|
8
|
+
import type { App } from "../App"
|
|
9
|
+
import type { EasiarrConfig } from "../../config/schema"
|
|
10
|
+
import {
|
|
11
|
+
composeUp,
|
|
12
|
+
composeDown,
|
|
13
|
+
composeStop,
|
|
14
|
+
composeRestart,
|
|
15
|
+
getContainerStatuses,
|
|
16
|
+
isDockerAvailable,
|
|
17
|
+
} from "../../docker"
|
|
18
|
+
import { createPageLayout } from "../components/PageLayout"
|
|
19
|
+
|
|
20
|
+
export class ContainerControl {
|
|
21
|
+
private renderer: RenderContext
|
|
22
|
+
private container: BoxRenderable
|
|
23
|
+
private app: App
|
|
24
|
+
private config: EasiarrConfig
|
|
25
|
+
|
|
26
|
+
constructor(renderer: RenderContext, container: BoxRenderable, app: App, config: EasiarrConfig) {
|
|
27
|
+
this.renderer = renderer
|
|
28
|
+
this.container = container
|
|
29
|
+
this.app = app
|
|
30
|
+
this.config = config
|
|
31
|
+
|
|
32
|
+
this.render()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async render(): Promise<void> {
|
|
36
|
+
const statuses = await getContainerStatuses()
|
|
37
|
+
const runningCount = statuses.filter((s) => s.status === "running").length
|
|
38
|
+
const totalCount = statuses.length
|
|
39
|
+
|
|
40
|
+
// Status in header info?
|
|
41
|
+
const statusText = `Status: ${runningCount}/${totalCount} Running`
|
|
42
|
+
|
|
43
|
+
// Check Docker availability first to possibly change status
|
|
44
|
+
const dockerOk = await isDockerAvailable()
|
|
45
|
+
const finalStepInfo = dockerOk ? statusText : "Internal Error: Docker Unavailable"
|
|
46
|
+
|
|
47
|
+
const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
|
|
48
|
+
title: "Container Control",
|
|
49
|
+
stepInfo: finalStepInfo,
|
|
50
|
+
footerHint: "Enter Select/Action q Back",
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (!dockerOk) {
|
|
54
|
+
content.add(
|
|
55
|
+
new TextRenderable(this.renderer, {
|
|
56
|
+
id: "docker-error",
|
|
57
|
+
content: "⚠ Docker is not available! Please check your Docker daemon.",
|
|
58
|
+
fg: "#ff6666",
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Spacer
|
|
64
|
+
content.add(
|
|
65
|
+
new TextRenderable(this.renderer, {
|
|
66
|
+
id: "spacer",
|
|
67
|
+
content: "",
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Show container list
|
|
72
|
+
if (statuses.length > 0) {
|
|
73
|
+
for (const status of statuses.slice(0, 12)) {
|
|
74
|
+
// Show a few more since we have space
|
|
75
|
+
const icon = status.status === "running" ? "🟢" : "🔴"
|
|
76
|
+
content.add(
|
|
77
|
+
new TextRenderable(this.renderer, {
|
|
78
|
+
id: `status-${status.name}`,
|
|
79
|
+
content: `${icon} ${status.name}`,
|
|
80
|
+
fg: status.status === "running" ? "#00cc66" : "#666666",
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
} else if (dockerOk) {
|
|
85
|
+
content.add(
|
|
86
|
+
new TextRenderable(this.renderer, {
|
|
87
|
+
id: "no-containers",
|
|
88
|
+
content: "No containers found. Run 'Start All' first.",
|
|
89
|
+
fg: "#888888",
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const menu = new SelectRenderable(this.renderer, {
|
|
95
|
+
id: "container-menu",
|
|
96
|
+
width: "100%",
|
|
97
|
+
height: 8, // Fixed height for actions at bottom
|
|
98
|
+
options: [
|
|
99
|
+
{ name: "▶ Start All", description: "docker compose up -d" },
|
|
100
|
+
{ name: "⏹ Stop All", description: "docker compose stop" },
|
|
101
|
+
{ name: "🔄 Restart All", description: "docker compose restart" },
|
|
102
|
+
{ name: "⬇ Down (Remove)", description: "docker compose down" },
|
|
103
|
+
{ name: "🔃 Refresh Status", description: "Update container list" },
|
|
104
|
+
{ name: "◀ Back to Main Menu", description: "" },
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
menu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
109
|
+
switch (index) {
|
|
110
|
+
case 0:
|
|
111
|
+
await composeUp()
|
|
112
|
+
break
|
|
113
|
+
case 1:
|
|
114
|
+
await composeStop()
|
|
115
|
+
break
|
|
116
|
+
case 2:
|
|
117
|
+
await composeRestart()
|
|
118
|
+
break
|
|
119
|
+
case 3:
|
|
120
|
+
await composeDown()
|
|
121
|
+
break
|
|
122
|
+
case 4:
|
|
123
|
+
// Refresh
|
|
124
|
+
break
|
|
125
|
+
case 5:
|
|
126
|
+
this.app.navigateTo("main")
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
// Refresh view - clear all children
|
|
130
|
+
const children = this.container.getChildren()
|
|
131
|
+
for (const child of children) {
|
|
132
|
+
this.container.remove(child.id)
|
|
133
|
+
}
|
|
134
|
+
this.render()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
content.add(menu)
|
|
138
|
+
menu.focus()
|
|
139
|
+
|
|
140
|
+
this.container.add(page)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Menu Screen
|
|
3
|
+
* Central navigation hub for Easiarr
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RenderContext, CliRenderer } from "@opentui/core"
|
|
7
|
+
import { BoxRenderable, TextRenderable, SelectRenderable, SelectRenderableEvents } from "@opentui/core"
|
|
8
|
+
import type { App } from "../App"
|
|
9
|
+
import type { EasiarrConfig } from "../../config/schema"
|
|
10
|
+
import { createPageLayout } from "../components/PageLayout"
|
|
11
|
+
import { saveCompose } from "../../compose"
|
|
12
|
+
import { ApiKeyViewer } from "./ApiKeyViewer"
|
|
13
|
+
import { AppConfigurator } from "./AppConfigurator"
|
|
14
|
+
|
|
15
|
+
export class MainMenu {
|
|
16
|
+
private renderer: RenderContext
|
|
17
|
+
private container: BoxRenderable
|
|
18
|
+
private app: App
|
|
19
|
+
private config: EasiarrConfig
|
|
20
|
+
private menu!: SelectRenderable
|
|
21
|
+
private page!: BoxRenderable
|
|
22
|
+
|
|
23
|
+
constructor(renderer: RenderContext, container: BoxRenderable, app: App, config: EasiarrConfig) {
|
|
24
|
+
this.renderer = renderer
|
|
25
|
+
this.container = container
|
|
26
|
+
this.app = app
|
|
27
|
+
this.config = config
|
|
28
|
+
|
|
29
|
+
this.render()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private render(): void {
|
|
33
|
+
const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
|
|
34
|
+
title: "Main Menu",
|
|
35
|
+
stepInfo: "Docker Compose Generator for *arr Ecosystem",
|
|
36
|
+
footerHint: "Enter Select Ctrl+C Exit",
|
|
37
|
+
})
|
|
38
|
+
this.page = page
|
|
39
|
+
|
|
40
|
+
// Config info
|
|
41
|
+
const configBox = new BoxRenderable(this.renderer, {
|
|
42
|
+
width: "100%",
|
|
43
|
+
flexDirection: "column",
|
|
44
|
+
marginBottom: 1,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
configBox.add(
|
|
48
|
+
new TextRenderable(this.renderer, {
|
|
49
|
+
id: "config-info-header",
|
|
50
|
+
content: "Configuration Overview:",
|
|
51
|
+
fg: "#4a9eff",
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
configBox.add(new BoxRenderable(this.renderer, { width: 1, height: 1 })) // Spacer
|
|
56
|
+
|
|
57
|
+
configBox.add(
|
|
58
|
+
new TextRenderable(this.renderer, {
|
|
59
|
+
id: "config-info",
|
|
60
|
+
content: ` 📁 Root: ${this.config.rootDir}`,
|
|
61
|
+
fg: "#aaaaaa",
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
configBox.add(
|
|
66
|
+
new TextRenderable(this.renderer, {
|
|
67
|
+
id: "apps-info",
|
|
68
|
+
content: ` Apps: ${this.config.apps.filter((a) => a.enabled).length} configured`,
|
|
69
|
+
fg: "#aaaaaa",
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
content.add(configBox)
|
|
74
|
+
|
|
75
|
+
content.add(new TextRenderable(this.renderer, { id: "spacer2", content: " " }))
|
|
76
|
+
|
|
77
|
+
// Menu
|
|
78
|
+
this.menu = new SelectRenderable(this.renderer, {
|
|
79
|
+
id: "main-menu-select",
|
|
80
|
+
width: "100%",
|
|
81
|
+
height: 10,
|
|
82
|
+
options: [
|
|
83
|
+
{
|
|
84
|
+
name: "📦 Manage Apps",
|
|
85
|
+
description: "Add, remove, or configure apps",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "🐳 Container Control",
|
|
89
|
+
description: "Start, stop, restart containers",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "⚙️ Advanced Settings",
|
|
93
|
+
description: "Customize ports, volumes, env",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "🔑 Extract API Keys",
|
|
97
|
+
description: "Find API keys from running containers",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "⚙️ Configure Apps",
|
|
101
|
+
description: "Set root folders and download clients via API",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "🔄 Regenerate Compose",
|
|
105
|
+
description: "Rebuild docker-compose.yml",
|
|
106
|
+
},
|
|
107
|
+
{ name: "❌ Exit", description: "Close easiarr" },
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
this.menu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
|
|
112
|
+
switch (index) {
|
|
113
|
+
case 0:
|
|
114
|
+
this.app.navigateTo("appManager")
|
|
115
|
+
break
|
|
116
|
+
case 1:
|
|
117
|
+
this.app.navigateTo("containerControl")
|
|
118
|
+
break
|
|
119
|
+
case 2:
|
|
120
|
+
this.app.navigateTo("advancedSettings")
|
|
121
|
+
break
|
|
122
|
+
case 3: {
|
|
123
|
+
// API Key Extractor
|
|
124
|
+
this.menu.blur()
|
|
125
|
+
this.page.visible = false
|
|
126
|
+
const viewer = new ApiKeyViewer(this.renderer as CliRenderer, this.config, () => {
|
|
127
|
+
// On Back
|
|
128
|
+
this.page.visible = true
|
|
129
|
+
this.menu.focus()
|
|
130
|
+
})
|
|
131
|
+
this.container.add(viewer)
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
case 4: {
|
|
135
|
+
// Configure Apps
|
|
136
|
+
this.menu.blur()
|
|
137
|
+
this.page.visible = false
|
|
138
|
+
const configurator = new AppConfigurator(this.renderer as CliRenderer, this.config, () => {
|
|
139
|
+
this.page.visible = true
|
|
140
|
+
this.menu.focus()
|
|
141
|
+
})
|
|
142
|
+
this.container.add(configurator)
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
case 5: {
|
|
146
|
+
// Regenerate compose
|
|
147
|
+
await saveCompose(this.config)
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
case 6:
|
|
151
|
+
process.exit(0)
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
content.add(this.menu)
|
|
157
|
+
this.menu.focus()
|
|
158
|
+
|
|
159
|
+
this.container.add(page)
|
|
160
|
+
}
|
|
161
|
+
}
|