1- // ── Streambert main process entry point ───────────────────────────────────────
1+ // -- Streambert main process entry point ---------------------------------------
22// Responsible for: window creation, session setup, ad-blocking, scheduled
33// backup trigger, and app lifecycle. All heavy IPC logic lives in src/ipc/.
44
@@ -12,7 +12,7 @@ const {
1212} = require ( "electron" ) ;
1313const path = require ( "path" ) ;
1414
15- // ── RAM / performance flags ───────────────────────────────────────────────────
15+ // -- RAM / performance flags ---------------------------------------------------
1616app . commandLine . appendSwitch (
1717 "js-flags" ,
1818 "--max-old-space-size=256 --expose-gc" ,
@@ -30,20 +30,20 @@ app.commandLine.appendSwitch("enable-features", "NetworkServiceInProcess2");
3030app . commandLine . appendSwitch ( "disk-cache-size" , String ( 80 * 1024 * 1024 ) ) ;
3131app . commandLine . appendSwitch ( "renderer-process-limit" , "3" ) ;
3232
33- // ── Startup benchmark ─────────────────────────────────────────────────────────
33+ // -- Startup benchmark ---------------------------------------------------------
3434const _t0 = Date . now ( ) ;
3535const _bench = ( label ) =>
3636 console . log ( `[boot] ${ label } : +${ Date . now ( ) - _t0 } ms` ) ;
3737
38- // ── Sub-modules ───────────────────────────────────────────────────────────────
38+ // -- Sub-modules ---------------------------------------------------------------
3939const blockStats = require ( "./src/ipc/blockStats" ) ;
4040const storageIpc = require ( "./src/ipc/storage" ) ;
4141const downloadsIpc = require ( "./src/ipc/downloads" ) ;
4242const subtitlesIpc = require ( "./src/ipc/subtitles" ) ;
4343const allmangaIpc = require ( "./src/ipc/allmanga" ) ;
4444const playerIpc = require ( "./src/ipc/player" ) ;
4545
46- // ── Ad/tracker block list ─────────────────────────────────────────────────────
46+ // -- Ad/tracker block list -----------------------------------------------------
4747const BLOCKED_HOSTS = [
4848 "*://www.google-analytics.com/*" ,
4949 "*://analytics.google.com/*" ,
@@ -98,11 +98,12 @@ const BLOCKED_HOSTS = [
9898 "*://tmstr4.neonhorizonworkshops.com/*" ,
9999] ;
100100
101- // ── Module-level state ────────────────────────────────────────────────────────
101+ // -- Module-level state --------------------------------------------------------
102102let mainWindow = null ;
103103const getMainWindow = ( ) => mainWindow ;
104104
105105const playerWcIds = new Set ( ) ;
106+ let sessionsConfigured = false ;
106107
107108function setupSession ( playerSession , trailerSession ) {
108109 const stripHeaders = ( details , callback ) => {
@@ -238,10 +239,9 @@ function createWindow() {
238239 } ,
239240 ) ;
240241
241- // ── Lazy session setup ────────────────────────────────────────────────────
242- // Player/trailer sessions are only configured on the first webview attach
243- // (i.e. when the user actually opens a movie/trailer), not at startup.
244- let sessionsConfigured = false ;
242+ // -- Lazy session setup ----------------------------------------------------
243+ // Player/trailer sessions are configured on the first webview attach or
244+ // when the pop-out window opens, whichever comes first.
245245
246246 // Block popups from webviews, intercept fullscreen, lazy-init sessions
247247 mainWindow . webContents . on ( "did-attach-webview" , ( _ , wc ) => {
@@ -307,7 +307,7 @@ function createWindow() {
307307 } ) ;
308308}
309309
310- // ── Register all IPC modules ──────────────────────────────────────────────────
310+ // -- Register all IPC modules --------------------------------------------------
311311storageIpc . register ( ) ;
312312downloadsIpc . register ( getMainWindow ) ;
313313subtitlesIpc . register ( {
@@ -323,7 +323,7 @@ blockStats.init(getMainWindow);
323323// get-block-stats lives with its data
324324ipcMain . handle ( "get-block-stats" , ( ) => blockStats . getBlockStats ( ) ) ;
325325
326- // ── Player memory cleanup ─────────────────────────────────────────────
326+ // -- Player memory cleanup ---------------------------------------------
327327// Called by MoviePage / TVPage on component unmount.
328328// Destroys the player webview WebContents by tracked ID, then flushes caches and GCs.
329329ipcMain . on ( "player-stopped" , ( ) => {
@@ -360,7 +360,7 @@ ipcMain.on("player-stopped", () => {
360360 }
361361} ) ;
362362
363- // ── Wyzie API Key Redemption Window ──────────────────────────────────────────
363+ // -- Wyzie API Key Redemption Window ------------------------------------------
364364// Opens https://sub.wyzie.io/redeem in a child BrowserWindow, watches the DOM
365365// for the api-key-display element, extracts the key, and sends it back.
366366ipcMain . handle ( "wyzie-open-redeem" , async ( ) => {
@@ -437,7 +437,7 @@ ipcMain.handle("wyzie-open-redeem", async () => {
437437 } ) ;
438438} ) ;
439439
440- // ── Wyzie API Key Validation ──────────────────────────────────────────────────
440+ // -- Wyzie API Key Validation --------------------------------------------------
441441ipcMain . handle ( "wyzie-validate-key" , async ( _ , key ) => {
442442 try {
443443 const controller = new AbortController ( ) ;
@@ -455,7 +455,7 @@ ipcMain.handle("wyzie-validate-key", async (_, key) => {
455455 }
456456} ) ;
457457
458- // ── Desktop notifications ─────────────────────────────────────────────────────
458+ // -- Desktop notifications -----------------------------------------------------
459459// Called from the renderer whenever it wants a native OS notification.
460460ipcMain . handle (
461461 "show-notification" ,
@@ -472,7 +472,111 @@ ipcMain.handle(
472472 } ,
473473) ;
474474
475- // ── Single-instance lock ──────────────────────────────────────────────────────
475+ // -- Picture-in-Picture / Pop-Out window --------------------------------------
476+ // Opens the player URL in a small always-on-top BrowserWindow (full site UI,
477+ // with subtitles and controls). The Main Window closes the stream to avoid duplication.
478+ let pipWindow = null ;
479+ const getPipWindow = ( ) => pipWindow ;
480+
481+ ipcMain . handle ( "open-pip-window" , ( _ , { url, title } ) => {
482+ if ( ! url || url === "about:blank" ) return { ok : false , reason : "no-url" } ;
483+
484+ // Guarantee tracker/ad blocking is active in persist:player before any load
485+ if ( ! sessionsConfigured ) {
486+ sessionsConfigured = true ;
487+ const playerSession = session . fromPartition ( "persist:player" ) ;
488+ const trailerSession = session . fromPartition ( "persist:trailer" ) ;
489+ setupSession ( playerSession , trailerSession ) ;
490+ }
491+
492+ if ( pipWindow && ! pipWindow . isDestroyed ( ) ) {
493+ pipWindow . loadURL ( url ) ;
494+ pipWindow . focus ( ) ;
495+ return { ok : true } ;
496+ }
497+
498+ pipWindow = new BrowserWindow ( {
499+ width : 640 ,
500+ height : 360 ,
501+ minWidth : 320 ,
502+ minHeight : 180 ,
503+ alwaysOnTop : true ,
504+ title : title ? `${ title } - Pop-out` : "Pop-out Player" ,
505+ backgroundColor : "#000000" ,
506+ // Same custom title bar as the main window
507+ titleBarStyle : process . platform === "darwin" ? "hiddenInset" : "hidden" ,
508+ frame : process . platform !== "win32" ,
509+ webPreferences : {
510+ partition : "persist:player" ,
511+ nodeIntegration : false ,
512+ contextIsolation : true ,
513+ // Injects the custom title bar and wires window-control IPC
514+ preload : path . join ( __dirname , "popout-preload.js" ) ,
515+ } ,
516+ } ) ;
517+
518+ // Block all popup windows from the streaming site and any nested frames
519+ pipWindow . webContents . setWindowOpenHandler ( ( ) => ( { action : "deny" } ) ) ;
520+
521+ // If the site uses <webview> elements (unlikely but safe), block there too
522+ pipWindow . webContents . on ( "did-attach-webview" , ( _ , wc ) => {
523+ wc . setWindowOpenHandler ( ( ) => ( { action : "deny" } ) ) ;
524+ } ) ;
525+
526+ pipWindow . loadURL ( url ) ;
527+
528+ // Push maximize state into the popout renderer so the title bar icon updates
529+ pipWindow . on ( "maximize" , ( ) => {
530+ if ( ! pipWindow . isDestroyed ( ) )
531+ pipWindow . webContents . send ( "popout-window-maximized" , true ) ;
532+ } ) ;
533+ pipWindow . on ( "unmaximize" , ( ) => {
534+ if ( ! pipWindow . isDestroyed ( ) )
535+ pipWindow . webContents . send ( "popout-window-maximized" , false ) ;
536+ } ) ;
537+
538+ const notifyMain = ( channel ) => {
539+ const mw = getMainWindow ( ) ;
540+ if ( mw && ! mw . isDestroyed ( ) ) mw . webContents . send ( channel ) ;
541+ } ;
542+
543+ pipWindow . on ( "closed" , ( ) => {
544+ pipWindow = null ;
545+ notifyMain ( "pip-window-closed" ) ;
546+ } ) ;
547+
548+ notifyMain ( "pip-window-opened" ) ;
549+ return { ok : true } ;
550+ } ) ;
551+
552+ ipcMain . handle ( "close-pip-window" , ( ) => {
553+ if ( pipWindow && ! pipWindow . isDestroyed ( ) ) pipWindow . close ( ) ;
554+ } ) ;
555+
556+ ipcMain . handle ( "get-pip-webcontents-id" , ( ) => {
557+ if ( pipWindow && ! pipWindow . isDestroyed ( ) ) return pipWindow . webContents . id ;
558+ return null ;
559+ } ) ;
560+
561+ // -- Popout window controls (used by popout-preload.js title bar buttons) -----
562+ ipcMain . handle ( "popout-window-minimize" , ( ) => {
563+ if ( pipWindow && ! pipWindow . isDestroyed ( ) ) pipWindow . minimize ( ) ;
564+ } ) ;
565+ ipcMain . handle ( "popout-window-toggle-maximize" , ( ) => {
566+ if ( ! pipWindow || pipWindow . isDestroyed ( ) ) return ;
567+ if ( pipWindow . isMaximized ( ) ) pipWindow . unmaximize ( ) ;
568+ else pipWindow . maximize ( ) ;
569+ } ) ;
570+ ipcMain . handle ( "popout-window-close" , ( ) => {
571+ if ( pipWindow && ! pipWindow . isDestroyed ( ) ) pipWindow . close ( ) ;
572+ } ) ;
573+ ipcMain . handle ( "popout-window-is-maximized" , ( ) => {
574+ return pipWindow && ! pipWindow . isDestroyed ( )
575+ ? pipWindow . isMaximized ( )
576+ : false ;
577+ } ) ;
578+
579+ // -- Single-instance lock ------------------------------------------------------
476580const gotTheLock = app . requestSingleInstanceLock ( ) ;
477581
478582if ( ! gotTheLock ) {
0 commit comments