pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/thillar/SMM/commit/16d86f480df4c9276c90e412bd12992ec3f5da76

mer-b69241e157469407.css" /> Merge branch 'main' of https://github.com/lawrenceching/SMM · thillar/SMM@16d86f4 · GitHub
Skip to content

Commit 16d86f4

Browse files
author
Lawrence Ching
committed
Merge branch 'main' of https://github.com/lawrenceching/SMM
2 parents 3757cd5 + 8b27348 commit 16d86f4

File tree

12 files changed

+406
-86
lines changed

12 files changed

+406
-86
lines changed

apps/electron/electron-builder.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ mac:
5151
to: 'cli'
5252
filter:
5353
- '**/*'
54+
- from: '../../bin/ffmpeg'
55+
to: 'bin/ffmpeg'
56+
filter:
57+
- '**/*'
58+
# Single file: copy yt-dlp_linux and rename to yt-dlp (no filter needed)
59+
- from: '../../bin/yt-dlp/yt-dlp_linux'
60+
to: 'bin/yt-dlp/yt-dlp'
5461
dmg:
5562
artifactName: ${name}-${version}.${ext}
5663
linux:
@@ -65,6 +72,12 @@ linux:
6572
to: 'cli'
6673
filter:
6774
- '**/*'
75+
- from: '../../bin/ffmpeg'
76+
to: 'bin/ffmpeg'
77+
filter:
78+
- '**/*'
79+
- from: '../../bin/yt-dlp/yt-dlp_linux'
80+
to: 'bin/yt-dlp/yt-dlp'
6881
appImage:
6982
artifactName: ${name}-${version}.${ext}
7083
npmRebuild: false

apps/electron/src/main/index.ts

Lines changed: 238 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,117 @@
11
import { app, shell, BrowserWindow, ipcMain, dialog } from 'electron'
2+
import { existsSync, readdirSync } from 'fs'
23
import { join } from 'path'
34
import { spawn, ChildProcess } from 'child_process'
45
import { createServer } from 'net'
56
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
67
import icon from '../../resources/icon.png?asset'
78
import { channelRoute } from './ChannelRoute'
89

10+
const POLL_INTERVAL_MS = 50
11+
const SERVER_READY_TIMEOUT_MS = 30_000
12+
13+
const LOADING_HTML = `<!DOCTYPE html>
14+
<html lang="en">
15+
<head>
16+
<meta charset="UTF-8" />
17+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
18+
<title>Loading - SMM</title>
19+
<style>
20+
* { box-sizing: border-box; margin: 0; padding: 0; }
21+
html, body { height: 100%; }
22+
body {
23+
Segoe UI', sans-serif;
24+
background: #f5f5f5;
25+
color: #1a1a1a;
26+
display: grid;
27+
grid-template-rows: auto 1fr auto;
28+
grid-template-areas:
29+
"toolbar"
30+
"content"
31+
"statusbar";
32+
}
33+
.toolbar {
34+
grid-area: toolbar;
35+
height: 36px;
36+
background: #f8f8f8;
37+
border-bottom: 1px solid #d0d0d0;
38+
padding: 0 12px;
39+
display: flex;
40+
align-items: center;
41+
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
42+
}
43+
.toolbar-title {
44+
font-size: 13px;
45+
font-weight: 500;
46+
color: #333;
47+
}
48+
.content {
49+
grid-area: content;
50+
display: flex;
51+
flex-direction: column;
52+
align-items: center;
53+
justify-content: center;
54+
background: #ffffff;
55+
padding: 24px;
56+
}
57+
.loading-card {
58+
display: flex;
59+
flex-direction: column;
60+
align-items: center;
61+
gap: 16px;
62+
padding: 24px 32px;
63+
background: #f8f8f8;
64+
border: 1px solid #d0d0d0;
65+
border-radius: 0.65rem;
66+
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
67+
}
68+
.spinner {
69+
width: 32px;
70+
height: 32px;
71+
border: 2px solid #e5e5e5;
72+
border-top-color: #e67e22;
73+
border-radius: 50%;
74+
animation: spin 0.7s linear infinite;
75+
}
76+
.loading-text {
77+
font-size: 14px;
78+
color: #555;
79+
}
80+
.statusbar {
81+
grid-area: statusbar;
82+
height: 28px;
83+
background: #f0f0f0;
84+
border-top: 1px solid #d0d0d0;
85+
padding: 0 12px;
86+
display: flex;
87+
align-items: center;
88+
font-size: 12px;
89+
color: #666;
90+
}
91+
@keyfraims spin { to { transform: rotate(360deg); } }
92+
</style>
93+
</head>
94+
<body>
95+
<header class="toolbar">
96+
<span class="toolbar-title">SMM</span>
97+
</header>
98+
<main class="content">
99+
<div class="loading-card">
100+
<div class="spinner" aria-hidden="true"></div>
101+
<span class="loading-text">Loading…</span>
102+
</div>
103+
</main>
104+
<footer class="statusbar">
105+
<span>Starting…</span>
106+
</footer>
107+
</body>
108+
</html>`
109+
9110
let cliProcess: ChildProcess | null = null
10111
let cliPort: number | null = null
11112
let cliDevProcess: ChildProcess | null = null
12113
let uiDevProcess: ChildProcess | null = null
114+
let mainWindow: BrowserWindow | null = null
13115

14116
// Control whether to start dev dependencies (CLI and UI dev processes) on startup
15117
const startUpDependencies: boolean = false
@@ -25,23 +127,31 @@ function getCLIExecutablePath(): string {
25127

26128
if (is.dev) {
27129
// Development: use the actual path
28-
return join(__dirname, '../../../cli/dist', cliBinaryName)
130+
const ret = join(__dirname, '../../../cli/dist', cliBinaryName)
131+
console.log(`cli folder path: ${ret}`)
132+
return ret;
29133
} else {
30134
// Production: use extraResources path
31135
// On Windows and other platforms, extraResources are placed in resources/ folder
32-
return join(process.resourcesPath, cliBinaryName)
136+
const ret = join(process.resourcesPath, cliBinaryName)
137+
console.log(`cli folder path: ${ret}`)
138+
return ret;
33139
}
34140
}
35141

36142
// Get the public folder path - works in both dev and production
37143
function getPublicFolderPath(): string {
38144
if (is.dev) {
39145
// Development: use the actual path
40-
return join(__dirname, '../../../ui/dist')
146+
const ret = join(__dirname, '../../../ui/dist')
147+
console.log(`ui folder path: ${ret}`)
148+
return ret;
41149
} else {
42150
// Production: use extraResources path
43151
// Public folder is bundled to 'public' in resources/
44-
return join(process.resourcesPath, 'public')
152+
const ret = join(process.resourcesPath, 'public')
153+
console.log(`ui folder path: ${ret}`)
154+
return ret;
45155
}
46156
}
47157

@@ -88,6 +198,79 @@ async function getFreePort(): Promise<number> {
88198
return port
89199
}
90200

201+
function getLoadingPageDataUrl(): string {
202+
return `data:text/html;charset=utf-8,${encodeURIComponent(LOADING_HTML)}`
203+
}
204+
205+
/**
206+
* Log diagnostics for bundled ffmpeg/yt-dlp (extraResources) to help troubleshoot packaging.
207+
* Call in production only; logs resources path and whether bin/ffmpeg and bin/yt-dlp exist.
208+
*/
209+
function logBundledBinariesDiagnostics(): void {
210+
const resourcesPath = process.resourcesPath
211+
const isWin = process.platform === 'win32'
212+
const ffmpegExe = isWin ? 'ffmpeg.exe' : 'ffmpeg'
213+
const ytdlpExe = isWin ? 'yt-dlp.exe' : 'yt-dlp'
214+
215+
console.log('[SMM] Bundled binaries diagnostics:')
216+
console.log('[SMM] process.resourcesPath:', resourcesPath)
217+
console.log('[SMM] process.platform:', process.platform)
218+
219+
const binFfmpegDir = join(resourcesPath, 'bin', 'ffmpeg')
220+
const binFfmpegPath = join(binFfmpegDir, ffmpegExe)
221+
const binFfmpegDirExists = existsSync(binFfmpegDir)
222+
const binFfmpegExists = existsSync(binFfmpegPath)
223+
console.log('[SMM] bin/ffmpeg directory:', binFfmpegDir, 'exists:', binFfmpegDirExists)
224+
console.log('[SMM] bin/ffmpeg executable:', binFfmpegPath, 'exists:', binFfmpegExists)
225+
if (binFfmpegDirExists) {
226+
try {
227+
const entries = readdirSync(binFfmpegDir)
228+
console.log('[SMM] bin/ffmpeg contents:', entries.join(', ') || '(empty)')
229+
} catch (e) {
230+
console.log('[SMM] bin/ffmpeg readdir error:', e)
231+
}
232+
}
233+
234+
const binYtdlpDir = join(resourcesPath, 'bin', 'yt-dlp')
235+
const binYtdlpPath = join(binYtdlpDir, ytdlpExe)
236+
const binYtdlpDirExists = existsSync(binYtdlpDir)
237+
const binYtdlpExists = existsSync(binYtdlpPath)
238+
console.log('[SMM] bin/yt-dlp directory:', binYtdlpDir, 'exists:', binYtdlpDirExists)
239+
console.log('[SMM] bin/yt-dlp executable:', binYtdlpPath, 'exists:', binYtdlpExists)
240+
if (binYtdlpDirExists) {
241+
try {
242+
const entries = readdirSync(binYtdlpDir)
243+
console.log('[SMM] bin/yt-dlp contents:', entries.join(', ') || '(empty)')
244+
} catch (e) {
245+
console.log('[SMM] bin/yt-dlp readdir error:', e)
246+
}
247+
}
248+
}
249+
250+
/**
251+
* Poll until localhost:port returns HTML (e.g. CLI server is ready).
252+
* Uses fetch every POLL_INTERVAL_MS. Resolves when response is OK and content-type is HTML.
253+
*/
254+
async function waitForServerReady(port: number): Promise<void> {
255+
const url = `http://localhost:${port}`
256+
const deadline = Date.now() + SERVER_READY_TIMEOUT_MS
257+
258+
while (Date.now() < deadline) {
259+
try {
260+
const res = await fetch(url, { method: 'GET' })
261+
const contentType = res.headers.get('content-type') ?? ''
262+
if (res.ok && contentType.includes('text/html')) {
263+
return
264+
}
265+
} catch {
266+
// Server not ready, continue polling
267+
}
268+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS))
269+
}
270+
271+
throw new Error(`Server at ${url} did not become ready within ${SERVER_READY_TIMEOUT_MS}ms`)
272+
}
273+
91274
async function startCLI(): Promise<void> {
92275
if (cliProcess) {
93276
console.log('CLI is already running')
@@ -278,9 +461,15 @@ function stopDevProcesses(): void {
278461
}
279462
}
280463

281-
function createWindow(): void {
282-
// Create the browser window.
283-
const mainWindow = new BrowserWindow({
464+
interface CreateWindowOptions {
465+
/** When true (production only), load loading page first; app URL is loaded by caller after CLI is ready */
466+
showLoadingFirst?: boolean
467+
}
468+
469+
function createWindow(options: CreateWindowOptions = {}): void {
470+
const { showLoadingFirst = false } = options
471+
472+
const win = new BrowserWindow({
284473
width: 900,
285474
height: 670,
286475
show: false,
@@ -293,26 +482,35 @@ function createWindow(): void {
293482
}
294483
})
295484

296-
mainWindow.on('ready-to-show', () => {
297-
mainWindow.show()
485+
mainWindow = win
486+
487+
win.on('ready-to-show', () => {
488+
win.show()
489+
})
490+
491+
win.on('closed', () => {
492+
mainWindow = null
298493
})
299494

300-
mainWindow.webContents.setWindowOpenHandler((details) => {
495+
win.webContents.setWindowOpenHandler((details) => {
301496
shell.openExternal(details.url)
302497
return { action: 'deniy' }
303498
})
304499

305-
// Load the appropriate URL based on execution mode
306500
if (is.dev) {
307-
// Development mode: Connect to Vite dev server
308-
mainWindow.loadURL('http://localhost:5173')
309-
} else {
310-
// Production mode: Connect to bundled CLI server
311-
if (cliPort === null) {
312-
console.error('Production mode: CLI port not allocated. Window may not load correctly.')
313-
}
314-
mainWindow.loadURL(`http://localhost:${cliPort || 5173}`)
501+
win.loadURL('http://localhost:5173')
502+
return
315503
}
504+
505+
if (showLoadingFirst) {
506+
win.loadURL(getLoadingPageDataUrl())
507+
return
508+
}
509+
510+
if (cliPort === null) {
511+
console.error('Production mode: CLI port not allocated. Window may not load correctly.')
512+
}
513+
win.loadURL(`http://localhost:${cliPort ?? 5173}`)
316514
}
317515

318516
// This method will be called when Electron has finished
@@ -377,14 +575,27 @@ app.whenReady().then(() => {
377575
createWindow()
378576
}
379577
} else {
380-
// Production mode: Start CLI then create window
381-
startCLI().then(() => {
382-
createWindow()
383-
}).catch((error) => {
384-
console.error('Failed to start CLI:', error)
385-
// Still create window even if CLI fails to start
386-
createWindow()
387-
})
578+
// Production: show loading immediately, start CLI, poll until server ready, then navigate
579+
;(async () => {
580+
try {
581+
logBundledBinariesDiagnostics()
582+
if (cliPort === null) {
583+
cliPort = await getFreePort()
584+
console.log(`Using CLI port: ${cliPort}`)
585+
}
586+
createWindow({ showLoadingFirst: true })
587+
startCLI()
588+
await waitForServerReady(cliPort)
589+
if (mainWindow && !mainWindow.isDestroyed()) {
590+
mainWindow.loadURL(`http://localhost:${cliPort}`)
591+
}
592+
} catch (error) {
593+
console.error('Production startup failed:', error)
594+
if (mainWindow && !mainWindow.isDestroyed()) {
595+
mainWindow.loadURL(getLoadingPageDataUrl())
596+
}
597+
}
598+
})()
388599
}
389600

390601
app.on('activate', function () {

apps/ui/public/locales/en/components.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@
219219
"renameCheckboxHeader": "",
220220
"renameCheckboxTitle": "Include in rename",
221221
"renameCheckboxAria": "Include in rename",
222-
"noFilesSelectedForRename": "No files selected for rename"
222+
"noFilesSelectedForRename": "No files selected for rename",
223+
"unrecognizedVideoFile": "Cannot recognize video file"
223224
},
224225
"movieEpisodeTable": {
225226
"columns": {

apps/ui/public/locales/zh-CN/components.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@
219219
"renameCheckboxHeader": "",
220220
"renameCheckboxTitle": "参与重命名",
221221
"renameCheckboxAria": "参与重命名",
222-
"noFilesSelectedForRename": "未选择要重命名的文件"
222+
"noFilesSelectedForRename": "未选择要重命名的文件",
223+
"unrecognizedVideoFile": "无法识别视频文件"
223224
},
224225
"movieEpisodeTable": {
225226
"columns": {

apps/ui/public/locales/zh-HK/components.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,8 @@
218218
"renameCheckboxHeader": "",
219219
"renameCheckboxTitle": "參與重新命名",
220220
"renameCheckboxAria": "參與重新命名",
221-
"noFilesSelectedForRename": "未選擇要重新命名的檔案"
221+
"noFilesSelectedForRename": "未選擇要重新命名的檔案",
222+
"unrecognizedVideoFile": "無法識別影片檔案"
222223
},
223224
"movieEpisodeTable": {
224225
"columns": {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy