@@ -3,6 +3,7 @@ import React, { useState, useEffect, useCallback } from 'react';
33import { SettingsTab , WidgetSettings } from './settings' ;
44import { MIN_POLL_INTERVAL_MIN } from './constants' ;
55import { runScan } from './scanner' ;
6+ import { scanFreeWeekend , WeekendGame } from './scanner/freeweekend' ;
67type Empty = [ ] ;
78type StrIn = [ { payload : string } ] ;
89
@@ -12,6 +13,8 @@ const loadSettings = callable<Empty, string>('load_settings_ipc');
1213const _logPluginIPC = callable < StrIn , number > ( 'log_plugin' ) ;
1314const saveFreeGamesCache = callable < StrIn , number > ( 'save_free_games_cache_ipc' ) ;
1415const loadFreeGamesCache = callable < Empty , string > ( 'load_free_games_cache_ipc' ) ;
16+ const saveFreeWeekendCache = callable < StrIn , number > ( 'save_free_weekend_cache_ipc' ) ;
17+ const loadFreeWeekendCache = callable < Empty , string > ( 'load_free_weekend_cache_ipc' ) ;
1518const _loadWidgetIPC = callable < Empty , string > ( 'load_widget_settings_ipc' ) ;
1619const _saveWidgetIPC = callable < StrIn , number > ( 'save_widget_settings_ipc' ) ;
1720const popToasts = callable < Empty , string > ( 'pop_toasts_ipc' ) ;
@@ -476,7 +479,7 @@ async function startPolling(): Promise<void> {
476479 const wRaw = await withTimeout ( _loadWidgetIPC ( ) , 1000 , '' ) ;
477480 if ( wRaw ) {
478481 const w = JSON . parse ( wRaw ) ;
479- if ( w && ( w . filterMode === 'all' || w . filterMode === 'games' ) ) {
482+ if ( w && ( w . filterMode === 'all' || w . filterMode === 'games' || w . filterMode === 'weekend ') ) {
480483 cachedWidgetFilterMode = w . filterMode ;
481484 return ;
482485 }
@@ -618,6 +621,68 @@ async function startPolling(): Promise<void> {
618621 log ( `processGame error for ${ game . name } : ${ String ( e ) } ` ) ;
619622 }
620623 }
624+
625+ function showWeekendNotification ( game : WeekendGame ) : void {
626+ const untilStr = new Date ( game . until * 1000 )
627+ . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' } ) ;
628+ toaster . toast ( {
629+ title : 'Free Weekend!' ,
630+ body : `${ game . name } is free to play until ${ untilStr } .` ,
631+ logo : React . createElement ( 'img' , {
632+ src : `https://cdn.akamai.steamstatic.com/steam/apps/${ game . appid } /header.jpg` ,
633+ style : { width : '40px' , height : '40px' , objectFit : 'cover' , borderRadius : '4px' } ,
634+ } ) ,
635+ onClick : ( ) => { ( window as any ) . SteamClient ?. Apps ?. ShowStore ?.( game . appid , 0 ) ; } ,
636+ duration : 12000 ,
637+ sound : 1 ,
638+ playSound : true ,
639+ showToast : true ,
640+ } ) ;
641+ }
642+
643+ async function runWeekendScan ( ) : Promise < void > {
644+ try {
645+ const result = await scanFreeWeekend ( { info : ( m ) => log ( m ) , warn : ( m ) => log ( m ) } ) ;
646+ if ( ! result ) {
647+ log ( 'Weekend scan failed — keeping previous list' ) ;
648+ return ;
649+ }
650+
651+ let prev : WeekendGame [ ] = [ ] ;
652+ try {
653+ prev = JSON . parse ( await withTimeout ( loadFreeWeekendCache ( ) , 3000 , '[]' ) || '[]' ) ;
654+ } catch { }
655+ const prevIds = new Set ( prev . map ( ( g ) => g . appid ) ) ;
656+
657+ const nowSec = Date . now ( ) / 1000 ;
658+ const merged = [ ...result ] ;
659+ for ( const p of prev ) {
660+ if ( p . until > nowSec && ! merged . some ( ( g ) => g . appid === p . appid ) ) merged . push ( p ) ;
661+ }
662+
663+ try {
664+ await withTimeout ( saveFreeWeekendCache ( { payload : JSON . stringify ( merged ) } ) , 3000 , 0 ) ;
665+ } catch { }
666+ log ( `Weekend scan complete — ${ merged . length } game(s) playable for free` ) ;
667+
668+ let notify = true ;
669+ try {
670+ const sraw = await withTimeout ( loadSettings ( ) , 2000 , '' ) ;
671+ if ( sraw ) notify = ( { ...DEFAULTS , ...JSON . parse ( sraw ) } as Settings ) . notifyOnGrab ;
672+ } catch { }
673+ if ( ! notify ) return ;
674+
675+ for ( const g of result ) {
676+ if ( prevIds . has ( g . appid ) ) continue ;
677+ if ( isAlreadyInLibrary ( g . appid ) ) continue ;
678+ log ( `Free weekend detected: ${ g . name } (${ g . appid } )` ) ;
679+ showWeekendNotification ( g ) ;
680+ await new Promise ( ( r ) => setTimeout ( r , 1500 ) ) ;
681+ }
682+ } catch ( e ) {
683+ log ( `runWeekendScan error: ${ String ( e ) } ` ) ;
684+ }
685+ }
621686
622687 async function runOneScan ( ) : Promise < boolean > {
623688 await reloadState ( ) ;
@@ -737,6 +802,15 @@ async function startPolling(): Promise<void> {
737802 }
738803
739804 await triggerScan ( 'initial' ) ;
805+ const WEEKEND_SCAN_INTERVAL_MS = 6 * 60 * 60 * 1000 ;
806+ let lastWeekendScanMs = Date . now ( ) ;
807+ void runWeekendScan ( ) ;
808+ _trackInterval ( ( ) => {
809+ if ( Date . now ( ) - lastWeekendScanMs >= WEEKEND_SCAN_INTERVAL_MS ) {
810+ lastWeekendScanMs = Date . now ( ) ;
811+ void runWeekendScan ( ) ;
812+ }
813+ } , 10 * 60 * 1000 ) ;
740814
741815 const pollManualScanRequest = async ( ) => {
742816 const manualRequestedAt = await consumeManualScanRequest ( ) ;
0 commit comments