diff --git a/.gitignore b/.gitignore index 981913b..4f9308b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -/target -/stremio*.exe +engine.log +engine.err +/target +/stremio*.exe /libmpv-2.dll \ No newline at end of file diff --git a/setup/Stremio.iss b/setup/Stremio.iss index 884b68e..d1e66ab 100644 --- a/setup/Stremio.iss +++ b/setup/Stremio.iss @@ -1,225 +1,227 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - -#define MyAppName "Stremio" -#define MyAppExeName "stremio-shell-ng.exe" -#define MyAppExeLocation SourcePath + "..\target\x86_64-pc-windows-msvc\release\" + MyAppExeName -#define MyAppVersion() GetVersionComponents(MyAppExeLocation, Local[0], Local[1], Local[2], Local[3]), \ - Str(Local[0]) + "." + Str(Local[1]) + "." + Str(Local[2]) - -#define MyAppPublisher "Smart Code OOD" -#define MyAppCopyright "Copyright © " + GetDateTimeString('yyyy', '', '') + " " + MyAppPublisher -#define MyAppURL "https://www.stremio.com/" -#define MyAppGoodbyeURL "https://www.strem.io/goodbye" -#define AssocTorrentExt ".torrent" -#define AssocTorrentKey StringChange(MyAppName, " ", "") + AssocTorrentExt -#define AssocTorrentDesc "Bittorrent seed file" - -#define public Dependency_NoExampleSetup -#include "CodeDependencies.iss" - -[Setup] -; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{DD3870DA-AF3C-4C73-B010-72944AB610C6} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppCopyright={#MyAppCopyright} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -DefaultDirName={autopf}\{#MyAppName} -SetupMutex=StremioShellNgSetupsMutex,Global\StremioShellNgSetupsMutex -; Remove the following line to run in administrative install mode (install for all users.) -PrivilegesRequired=lowest -DisableReadyPage=yes -DisableDirPage=yes -DisableProgramGroupPage=yes -; DisableFinishedPage=yes -ChangesAssociations=yes -OutputBaseFilename={#MyAppName}Setup-v{#MyAppVersion} -OutputDir=.. -Compression=lzma -SolidCompression=yes -WizardStyle=modern -LanguageDetectionMethod=uilanguage -ShowLanguageDialog=auto -CloseApplications=yes -WizardImageFile={#SourcePath}..\images\windows-installer.bmp -WizardSmallImageFile={#SourcePath}..\images\windows-installer-header.bmp -SetupIconFile={#SourcePath}..\images\stremio.ico -UninstallDisplayIcon={app}\{#MyAppExeName},0 -#ifdef SIGN -SignTool=stremiosign -SignedUninstaller=yes -#endif - -[Code] -function InitializeSetup: Boolean; -begin - Dependency_AddWebView2; - Result := True; -end; - -function ShouldSkipPage(PageID: Integer): Boolean; -begin - { Hide finish page if run app is selected } - if (PageID = wpFinished) and WizardIsTaskSelected('runapp') then - Result := True - else - Result := False; -end; - -procedure CurPageChanged(CurPageID: Integer); -begin - case (CurPageID) of - wpSelectTasks: WizardForm.NextButton.Caption := SetupMessage(msgButtonInstall); - wpFinished: WizardForm.NextButton.Caption := SetupMessage(msgButtonFinish); - else - WizardForm.NextButton.Caption := SetupMessage(msgButtonNext); - end; -end; - -procedure CurStepChanged(CurStep: TSetupStep); -var - ResultCode: Integer; -begin - if (CurStep = ssDone) and WizardIsTaskSelected('runapp') then - ExecAsOriginalUser(ExpandConstant('{app}\{#MyAppExeName}'), '', '', SW_SHOW, ewNoWait, ResultCode); -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - ErrorCode: Integer; -begin - case (CurUninstallStep) of - usPostUninstall: if MsgBox(ExpandConstant('{cm:RemoveDataFolder}'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then - DelTree(ExpandConstant('{app}'), True, True, True); - usDone: ShellExec('', ExpandConstant('{#MyAppGoodbyeURL}'), '', '', SW_SHOW, ewNoWait, ErrorCode); - end; -end; - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" -Name: "armenian"; MessagesFile: "compiler:Languages\Armenian.isl" -Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" -Name: "bulgarian"; MessagesFile: "compiler:Languages\Bulgarian.isl" -Name: "catalan"; MessagesFile: "compiler:Languages\Catalan.isl" -Name: "corsican"; MessagesFile: "compiler:Languages\Corsican.isl" -Name: "czech"; MessagesFile: "compiler:Languages\Czech.isl" -Name: "danish"; MessagesFile: "compiler:Languages\Danish.isl" -Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl" -Name: "finnish"; MessagesFile: "compiler:Languages\Finnish.isl" -Name: "french"; MessagesFile: "compiler:Languages\French.isl" -Name: "german"; MessagesFile: "compiler:Languages\German.isl" -Name: "hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl" -Name: "icelandic"; MessagesFile: "compiler:Languages\Icelandic.isl" -Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" -Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" -Name: "norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl" -Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" -Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" -Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" -Name: "slovak"; MessagesFile: "compiler:Languages\Slovak.isl" -Name: "slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl" -Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" -Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" -Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl" - -[CustomMessages] -RemoveDataFolder=Remove all data and configuration? -english.RemoveDataFolder=Remove all data and configuration? -armenian.RemoveDataFolder=Հեռացնե՞լ բոլոր տվյալները և կոնֆիգուրացիան: -brazilianportuguese.RemoveDataFolder=Remover todos os dados e configuração? -bulgarian.RemoveDataFolder=Премахване на всички данни и конфигурация? -catalan.RemoveDataFolder=Vols suprimir totes les dades i la configuració? -corsican.RemoveDataFolder=Eliminate tutti i dati è a cunfigurazione? -czech.RemoveDataFolder=Odebrat všechna data a konfiguraci? -danish.RemoveDataFolder=Remove all data and configuration? -dutch.RemoveDataFolder=Remove all data and configuration? -finnish.RemoveDataFolder=Poistetaanko kaikki tiedot ja asetukset? -french.RemoveDataFolder=Supprimer toutes les données et la configuration ? -german.RemoveDataFolder=Alle Daten und Konfiguration entfernen? -hebrew.RemoveDataFolder=Remove all data and configuration? -icelandic.RemoveDataFolder=Fjarlægja öll gögn og stillingar? -italian.RemoveDataFolder=Rimuovere tutti i dati e la configurazione? -japanese.RemoveDataFolder=すべてのデータと構成を削除しますか? -norwegian.RemoveDataFolder=Vil du fjerne all data og konfigurasjon? -polish.RemoveDataFolder=Usunąć wszystkie dane i konfigurację? -portuguese.RemoveDataFolder=Remover todos os dados e configuração? -russian.RemoveDataFolder=Удалить все данные и конфигурацию? -slovak.RemoveDataFolder=Chcete odstrániť všetky údaje a konfiguráciu? -slovenian.RemoveDataFolder=Želite odstraniti vse podatke in konfiguracijo? -spanish.RemoveDataFolder=¿Eliminar todos los datos y la configuración? -turkish.RemoveDataFolder=Tüm veriler ve yapılandırma kaldırılsın mı? -ukrainian.RemoveDataFolder=Видалити всі дані та конфігурацію? - -[Tasks] -Name: "runapp"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}" -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}" -;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "assoctorrent"; Description: "Associate {#MyAppName} with .torrent files" - -[Files] -; NOTE: Don't use "Flags: ignoreversion" on any shared system files -Source: "{#MyAppExeLocation}"; DestDir: "{app}"; Flags: ignoreversion signonce -Source: "{#SourcePath}..\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion signonce -Source: "{#SourcePath}..\bin\ffmpeg.exe"; DestDir: "{app}"; Flags: ignoreversion signonce -Source: "{#SourcePath}..\bin\ffprobe.exe"; DestDir: "{app}"; Flags: ignoreversion signonce -Source: "{#SourcePath}..\bin\stremio-runtime.exe"; DestDir: "{app}"; Flags: ignoreversion signonce -Source: "{#SourcePath}..\server.js"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\avcodec-58.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\avdevice-58.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\avfilter-7.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\avformat-58.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\avutil-56.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\postproc-55.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\swresample-3.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\swscale-5.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#SourcePath}..\bin\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion - - -[Registry] -; Associate .torrent files if assoctorrent task is selected -Root: HKA; Subkey: "Software\Classes\{#AssocTorrentExt}}\OpenWithProgids"; ValueType: string; ValueName: "{#AssocTorrentKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: assoctorrent -Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}"; ValueType: string; ValueName: ""; ValueData: "{#AssocTorrentDesc}"; Flags: uninsdeletekey; Tasks: assoctorrent -Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: assoctorrent -Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: assoctorrent - -; stremio: protocol -Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: ""; ValueData: "URL:Stremio Protocol"; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\stremio\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\stremio\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey - -; magnet: protocol -Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: ""; ValueData: "URL:BitTorrent magnet"; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\magnet\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\magnet\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey - -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".torrent"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".avi"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".asf"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mkv"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mp4"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mov"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogv"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".wmv"; ValueData: ""; Flags: uninsdeletekey -Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".srt"; ValueData: ""; Flags: uninsdeletekey - -[Icons] -Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - -; This is used if the desktop shortcut is created by the [run] section. -; [UninstallDelete] -; Type: files; Name: "{autodesktop}\{#MyAppName}.lnk" - -; We don't use the run section as the .torrent association is very hard to handle -; [Run] -; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent -; Filename: "cmd"; Parameters: "/c copy ""{autoprograms}\{#MyAppName}.lnk"" ""{autodesktop}"""; Description: "{cm:CreateDesktopIcon}"; Flags: postinstall skipifsilent shellexec runhidden waituntilterminated runascurrentuser +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Stremio BorderBreaker" +#define MyAppExeName "stremio-shell-ng.exe" +#define MyAppExeLocation SourcePath + "..\target\x86_64-pc-windows-msvc\release\" + MyAppExeName +#define MyAppVersion() GetVersionComponents(MyAppExeLocation, Local[0], Local[1], Local[2], Local[3]), \ + Str(Local[0]) + "." + Str(Local[1]) + "." + Str(Local[2]) + +#define MyAppPublisher "BorderBreaker" +#define MyAppCopyright "Copyright © " + GetDateTimeString('yyyy', '', '') + " " + MyAppPublisher +#define MyAppURL "https://stremio-borderbreaker.local/" +#define MyAppGoodbyeURL MyAppURL +#define AssocTorrentExt ".torrent" +#define AssocTorrentKey StringChange(MyAppName, " ", "") + AssocTorrentExt +#define AssocTorrentDesc "Bittorrent seed file" + +#define public Dependency_NoExampleSetup +#include "CodeDependencies.iss" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{5C3D0D2C-5B0D-4567-90A6-5D21C4EF8B52} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppCopyright={#MyAppCopyright} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +SetupMutex=StremioShellNgSetupsMutex,Global\StremioShellNgSetupsMutex +; Remove the following line to run in administrative install mode (install for all users.) +PrivilegesRequired=lowest +DisableReadyPage=yes +DisableDirPage=yes +DisableProgramGroupPage=yes +; DisableFinishedPage=yes +ChangesAssociations=yes +OutputBaseFilename={#MyAppName}Setup-v{#MyAppVersion} +OutputDir=.. +Compression=lzma +SolidCompression=yes +WizardStyle=modern +LanguageDetectionMethod=uilanguage +ShowLanguageDialog=auto +CloseApplications=yes +WizardImageFile={#SourcePath}..\images\windows-installer.bmp +WizardSmallImageFile={#SourcePath}..\images\windows-installer-header.bmp +SetupIconFile={#SourcePath}..\images\stremio.ico +UninstallDisplayIcon={app}\{#MyAppExeName},0 +#ifdef SIGN +SignTool=stremiosign +SignedUninstaller=yes +#endif + +[Code] +function InitializeSetup: Boolean; +begin + Dependency_AddWebView2; + Result := True; +end; + +function ShouldSkipPage(PageID: Integer): Boolean; +begin + { Hide finish page if run app is selected } + if (PageID = wpFinished) and WizardIsTaskSelected('runapp') then + Result := True + else + Result := False; +end; + +procedure CurPageChanged(CurPageID: Integer); +begin + case (CurPageID) of + wpSelectTasks: WizardForm.NextButton.Caption := SetupMessage(msgButtonInstall); + wpFinished: WizardForm.NextButton.Caption := SetupMessage(msgButtonFinish); + else + WizardForm.NextButton.Caption := SetupMessage(msgButtonNext); + end; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + ResultCode: Integer; +begin + if (CurStep = ssDone) and WizardIsTaskSelected('runapp') then + ExecAsOriginalUser(ExpandConstant('{app}\{#MyAppExeName}'), '', '', SW_SHOW, ewNoWait, ResultCode); +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + ErrorCode: Integer; +begin + case (CurUninstallStep) of + usPostUninstall: if MsgBox(ExpandConstant('{cm:RemoveDataFolder}'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then + DelTree(ExpandConstant('{app}'), True, True, True); + usDone: ShellExec('', ExpandConstant('{#MyAppGoodbyeURL}'), '', '', SW_SHOW, ewNoWait, ErrorCode); + end; +end; + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "armenian"; MessagesFile: "compiler:Languages\Armenian.isl" +Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" +Name: "bulgarian"; MessagesFile: "compiler:Languages\Bulgarian.isl" +Name: "catalan"; MessagesFile: "compiler:Languages\Catalan.isl" +Name: "corsican"; MessagesFile: "compiler:Languages\Corsican.isl" +Name: "czech"; MessagesFile: "compiler:Languages\Czech.isl" +Name: "danish"; MessagesFile: "compiler:Languages\Danish.isl" +Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl" +Name: "finnish"; MessagesFile: "compiler:Languages\Finnish.isl" +Name: "french"; MessagesFile: "compiler:Languages\French.isl" +Name: "german"; MessagesFile: "compiler:Languages\German.isl" +Name: "hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl" +Name: "hungarian"; MessagesFile: "compiler:Languages\Hungarian.isl" +Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" +Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" +Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl" +Name: "norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl" +Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" +Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" +Name: "slovak"; MessagesFile: "compiler:Languages\Slovak.isl" +Name: "slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl" +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" +Name: "swedish"; MessagesFile: "compiler:Languages\Swedish.isl" +Name: "tamil"; MessagesFile: "compiler:Languages\Tamil.isl" +Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" +Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl" + +[CustomMessages] +RemoveDataFolder=Remove all data and configuration? +english.RemoveDataFolder=Remove all data and configuration? +armenian.RemoveDataFolder=Հեռացնե՞լ բոլոր տվյալները և կոնֆիգուրացիան: +brazilianportuguese.RemoveDataFolder=Remover todos os dados e configuração? +bulgarian.RemoveDataFolder=Премахване на всички данни и конфигурация? +catalan.RemoveDataFolder=Vols suprimir totes les dades i la configuració? +corsican.RemoveDataFolder=Eliminate tutti i dati è a cunfigurazione? +czech.RemoveDataFolder=Odebrat všechna data a konfiguraci? +danish.RemoveDataFolder=Remove all data and configuration? +dutch.RemoveDataFolder=Remove all data and configuration? +finnish.RemoveDataFolder=Poistetaanko kaikki tiedot ja asetukset? +french.RemoveDataFolder=Supprimer toutes les données et la configuration ? +german.RemoveDataFolder=Alle Daten und Konfiguration entfernen? +hebrew.RemoveDataFolder=Remove all data and configuration? +italian.RemoveDataFolder=Rimuovere tutti i dati e la configurazione? +japanese.RemoveDataFolder=すべてのデータと構成を削除しますか? +norwegian.RemoveDataFolder=Vil du fjerne all data og konfigurasjon? +polish.RemoveDataFolder=Usunąć wszystkie dane i konfigurację? +portuguese.RemoveDataFolder=Remover todos os dados e configuração? +russian.RemoveDataFolder=Удалить все данные и конфигурацию? +slovak.RemoveDataFolder=Chcete odstrániť všetky údaje a konfiguráciu? +slovenian.RemoveDataFolder=Želite odstraniti vse podatke in konfiguracijo? +spanish.RemoveDataFolder=¿Eliminar todos los datos y la configuración? +turkish.RemoveDataFolder=Tüm veriler ve yapılandırma kaldırılsın mı? +ukrainian.RemoveDataFolder=Видалити всі дані та конфігурацію? + +[Tasks] +Name: "runapp"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}" +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}" +;Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "assoctorrent"; Description: "Associate {#MyAppName} with .torrent files" + +[Files] +; NOTE: Don't use "Flags: ignoreversion" on any shared system files +Source: "{#MyAppExeLocation}"; DestDir: "{app}"; Flags: ignoreversion signonce +Source: "{#SourcePath}..\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion signonce +Source: "{#SourcePath}..\bin\ffmpeg.exe"; DestDir: "{app}"; Flags: ignoreversion signonce +Source: "{#SourcePath}..\bin\ffprobe.exe"; DestDir: "{app}"; Flags: ignoreversion signonce +Source: "{#SourcePath}..\bin\stremio-runtime.exe"; DestDir: "{app}"; Flags: ignoreversion signonce +Source: "{#SourcePath}..\server.js"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\avcodec-58.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\avdevice-58.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\avfilter-7.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\avformat-58.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\avutil-56.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\postproc-55.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\swresample-3.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\swscale-5.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#SourcePath}..\bin\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion + + +[Registry] +; Associate .torrent files if assoctorrent task is selected +Root: HKA; Subkey: "Software\Classes\{#AssocTorrentExt}}\OpenWithProgids"; ValueType: string; ValueName: "{#AssocTorrentKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: assoctorrent +Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}"; ValueType: string; ValueName: ""; ValueData: "{#AssocTorrentDesc}"; Flags: uninsdeletekey; Tasks: assoctorrent +Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey; Tasks: assoctorrent +Root: HKA; Subkey: "Software\Classes\{#AssocTorrentKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: assoctorrent + +; stremio: protocol +Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: ""; ValueData: "URL:Stremio Protocol"; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\stremio"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\stremio\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\stremio\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey + +; magnet: protocol +Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: ""; ValueData: "URL:BitTorrent magnet"; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\magnet"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\magnet\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\magnet\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey + +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".torrent"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".avi"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".asf"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mkv"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mp4"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".mov"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".ogv"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".wmv"; ValueData: ""; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".srt"; ValueData: ""; Flags: uninsdeletekey + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +; This is used if the desktop shortcut is created by the [run] section. +; [UninstallDelete] +; Type: files; Name: "{autodesktop}\{#MyAppName}.lnk" + +; We don't use the run section as the .torrent association is very hard to handle +; [Run] +; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent +; Filename: "cmd"; Parameters: "/c copy ""{autoprograms}\{#MyAppName}.lnk"" ""{autodesktop}"""; Description: "{cm:CreateDesktopIcon}"; Flags: postinstall skipifsilent shellexec runhidden waituntilterminated runascurrentuser diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index 4c1adcd..53ae784 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -1,7 +1,7 @@ use native_windows_derive::NwgUi; use native_windows_gui as nwg; use rand::Rng; -use serde_json; +use serde_json::{self, json}; use std::{ cell::RefCell, io::Read, @@ -16,6 +16,7 @@ use url::Url; use winapi::um::{winbase::CREATE_BREAKAWAY_FROM_JOB, winuser::WS_EX_TOPMOST}; use crate::stremio_app::{ + aspect_ratio::AspectController, constants::{APP_NAME, UPDATE_ENDPOINT, UPDATE_INTERVAL, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH}, ipc::{RPCRequest, RPCResponse}, splash::SplashImage, @@ -84,6 +85,11 @@ pub struct MainWindow { #[nwg_control] #[nwg_events(OnNotice: [Self::on_focus_notice] )] pub focus_notice: nwg::Notice, + #[nwg_control] + #[nwg_events(OnNotice: [Self::on_aspect_toggle_notice] )] + pub aspect_toggle_notice: nwg::Notice, + pub aspect_controller: RefCell, + pub aspect_player_tx: RefCell>>, } impl MainWindow { @@ -135,13 +141,16 @@ impl MainWindow { self.window.set_visible(!self.start_hidden); self.tray.tray_show_hide.set_checked(!self.start_hidden); - let player_channel = self.player.channel.borrow(); let (player_tx, player_rx) = player_channel .as_ref() .expect("Cannont obtain communication channel for the Player"); let player_tx = player_tx.clone(); let player_rx = player_rx.clone(); + { + *self.aspect_player_tx.borrow_mut() = Some(player_tx.clone()); + self.aspect_controller.borrow().apply_current(&player_tx); + } let web_channel = self.webview.channel.borrow(); let (web_tx, web_rx) = web_channel @@ -242,6 +251,7 @@ impl MainWindow { let hide_splash_sender = self.hide_splash_notice.sender(); let focus_sender = self.focus_notice.sender(); let autoupdater_setup_mutex = self.autoupdater_setup_file.clone(); + let aspect_toggle_sender_thread = self.aspect_toggle_notice.sender(); thread::spawn(move || loop { if let Some(msg) = web_rx .recv() @@ -296,6 +306,9 @@ impl MainWindow { Some("win-focus") => { focus_sender.notice(); } + Some("borderbreaker-cycle") => { + aspect_toggle_sender_thread.notice(); + } Some("autoupdater-notif-clicked") => { // We've shown the "Update Available" notification // and the user clicked on "Restart And Update" @@ -421,4 +434,31 @@ impl MainWindow { self.tray.tray_show_hide.set_checked(self.window.visible()); self.transmit_window_visibility_change(); } + fn on_aspect_toggle_notice(&self) { + let player_tx_opt = self.aspect_player_tx.borrow().clone(); + if let Some(player_tx) = player_tx_opt { + let mut controller = self.aspect_controller.borrow_mut(); + controller.cycle(); + controller.apply_current(&player_tx); + let label = format!( + "Aspect: {}", + controller + .current_mode() + .overlay_label(controller.display_ratio()) + ); + drop(controller); + self.broadcast_aspect_overlay(&label); + } + } + fn broadcast_aspect_overlay(&self, text: &str) { + if let Ok(web_channel) = self.webview.channel.try_borrow() { + if let Some((web_tx, _)) = web_channel.as_ref() { + let payload = json!({ + "__borderbreakerOverlay": text, + }) + .to_string(); + let _ = web_tx.send(payload); + } + } + } } diff --git a/src/stremio_app/aspect_ratio.rs b/src/stremio_app/aspect_ratio.rs new file mode 100644 index 0000000..080ef4e --- /dev/null +++ b/src/stremio_app/aspect_ratio.rs @@ -0,0 +1,286 @@ +use std::{ + env, + fs::{self, File}, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use crate::stremio_app::{ + stremio_player::{BoolProp, FpProp, InMsg, InMsgArgs, InMsgFn, PropKey, PropVal, StrProp}, + window_helper, +}; +use flume::Sender; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +static CONFIG_DIR: Lazy = Lazy::new(|| { + env::var("APPDATA") + .map(PathBuf::from) + .unwrap_or_else(|_| env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) + .join("StremioBorderBreaker") +}); + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum AspectMode { + AutoDetect, + FillCrop, + FitToScreen, + Ratio16x9, + Ratio4x3, + Ratio1x1, + Ratio21x9, + Ratio32x9, + Cinema, +} + +impl AspectMode { + pub fn display_name(self) -> &'static str { + match self { + AspectMode::AutoDetect => "Auto", + AspectMode::FillCrop => "Fill (Crop)", + AspectMode::FitToScreen => "Fit to Screen", + AspectMode::Ratio16x9 => "16:9", + AspectMode::Ratio4x3 => "4:3", + AspectMode::Ratio1x1 => "1:1", + AspectMode::Ratio21x9 => "21:9 Ultrawide", + AspectMode::Ratio32x9 => "32:9 Super Ultrawide", + AspectMode::Cinema => "Cinema", + } + } + + pub fn overlay_label(self, display_ratio: f32) -> String { + match self { + AspectMode::AutoDetect => { + format!("Auto ({:.2}:1)", display_ratio.max(0.01)) + } + mode => mode.display_name().to_string(), + } + } +} + +struct AspectSpec { + aspect_override: Option, + keep_aspect: bool, + panscan: f64, + video_unscaled: Option<&'static str>, +} + +impl AspectMode { + fn spec(self, display_ratio: f32) -> AspectSpec { + match self { + AspectMode::AutoDetect => AspectSpec { + aspect_override: Some(display_ratio.max(0.1) as f64), + keep_aspect: true, + panscan: 0.0, + video_unscaled: Some("no"), + }, + AspectMode::FillCrop => AspectSpec { + aspect_override: None, + keep_aspect: true, + panscan: 1.0, + video_unscaled: Some("no"), + }, + AspectMode::FitToScreen => AspectSpec { + aspect_override: None, + keep_aspect: true, + panscan: 0.0, + video_unscaled: Some("no"), + }, + AspectMode::Ratio16x9 => AspectSpec::ratio(16.0 / 9.0), + AspectMode::Ratio4x3 => AspectSpec::ratio(4.0 / 3.0), + AspectMode::Ratio1x1 => AspectSpec::ratio(1.0), + AspectMode::Ratio21x9 => AspectSpec::ratio(21.0 / 9.0), + AspectMode::Ratio32x9 => AspectSpec::ratio(32.0 / 9.0), + AspectMode::Cinema => AspectSpec::ratio(2.39), + } + } +} + +impl AspectSpec { + fn ratio(value: f64) -> Self { + AspectSpec { + aspect_override: Some(value), + keep_aspect: true, + panscan: 0.0, + video_unscaled: Some("no"), + } + } +} + +#[derive(Serialize, Deserialize)] +struct AspectConfig { + mode: AspectMode, +} + +pub struct AspectController { + config_path: PathBuf, + order: Vec, + current_index: usize, + display_ratio: f32, +} + +impl AspectController { + pub fn new() -> Self { + Self::with_paths( + CONFIG_DIR.join("aspect.json"), + window_helper::primary_monitor_ratio(), + ) + } + + fn with_paths(config_path: PathBuf, display_ratio: f32) -> Self { + let order = vec![ + AspectMode::AutoDetect, + AspectMode::FillCrop, + AspectMode::FitToScreen, + AspectMode::Ratio16x9, + AspectMode::Ratio4x3, + AspectMode::Ratio1x1, + AspectMode::Ratio21x9, + AspectMode::Ratio32x9, + AspectMode::Cinema, + ]; + let saved_mode = Self::read_config(&config_path).map(|c| c.mode); + let current_index = saved_mode + .and_then(|mode| order.iter().position(|m| m == &mode)) + .unwrap_or(0); + AspectController { + config_path, + order, + current_index, + display_ratio, + } + } + + pub fn current_mode(&self) -> AspectMode { + self.order[self.current_index] + } + + pub fn cycle(&mut self) -> AspectMode { + self.current_index = (self.current_index + 1) % self.order.len(); + self.persist(); + self.current_mode() + } + + pub fn apply_current(&self, player_tx: &Sender) { + self.apply_mode(player_tx, self.current_mode()); + } + + pub fn apply_mode(&self, player_tx: &Sender, mode: AspectMode) { + let spec = mode.spec(self.display_ratio); + if let Some(ratio) = spec.aspect_override { + send_fp_prop(player_tx, FpProp::VideoAspectOverride, ratio); + } else { + send_fp_prop(player_tx, FpProp::VideoAspectOverride, 0.0); + } + send_bool_prop(player_tx, BoolProp::Keepaspect, spec.keep_aspect); + send_fp_prop(player_tx, FpProp::Panscan, spec.panscan); + if let Some(value) = spec.video_unscaled { + send_str_prop(player_tx, StrProp::VideoUnscaled, value); + } + } + + pub fn display_ratio(&self) -> f32 { + self.display_ratio + } + + fn persist(&self) { + if let Some(parent) = self.config_path.parent() { + let _ = fs::create_dir_all(parent); + } + let config = AspectConfig { + mode: self.current_mode(), + }; + if let Ok(data) = serde_json::to_vec(&config) { + if let Ok(mut file) = File::create(&self.config_path) { + let _ = file.write_all(&data); + } + } + } + + fn read_config(path: &Path) -> Option { + let mut file = File::open(path).ok()?; + let mut buf = Vec::new(); + file.read_to_end(&mut buf).ok()?; + serde_json::from_slice(&buf).ok() + } +} + +impl Default for AspectController { + fn default() -> Self { + Self::new() + } +} + +fn send_fp_prop(player_tx: &Sender, prop: FpProp, value: f64) { + let msg = InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(PropKey::Fp(prop), PropVal::Num(value)), + ); + if let Ok(serialized) = serde_json::to_string(&msg) { + let _ = player_tx.send(serialized); + } +} + +fn send_bool_prop(player_tx: &Sender, prop: BoolProp, value: bool) { + let msg = InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(PropKey::Bool(prop), PropVal::Bool(value)), + ); + if let Ok(serialized) = serde_json::to_string(&msg) { + let _ = player_tx.send(serialized); + } +} + +fn send_str_prop(player_tx: &Sender, prop: StrProp, value: &str) { + let msg = InMsg( + InMsgFn::MpvSetProp, + InMsgArgs::StProp(PropKey::Str(prop), PropVal::Str(value.to_string())), + ); + if let Ok(serialized) = serde_json::to_string(&msg) { + let _ = player_tx.send(serialized); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::{distributions::Alphanumeric, Rng}; + use std::fs; + + fn temp_config_path() -> PathBuf { + let mut name: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect(); + name.push_str(".json"); + env::temp_dir().join(name) + } + + #[test] + fn cycles_modes_and_persists() { + let path = temp_config_path(); + let mut controller = AspectController::with_paths(path.clone(), 2.33); + assert_eq!(controller.current_mode(), AspectMode::AutoDetect); + controller.cycle(); + assert_eq!(controller.current_mode(), AspectMode::FillCrop); + controller.cycle(); + assert_eq!(controller.current_mode(), AspectMode::FitToScreen); + // ensure persisted + let loaded = AspectController::with_paths(path.clone(), 2.33); + assert_eq!(loaded.current_mode(), AspectMode::FitToScreen); + let _ = fs::remove_file(path); + } + + #[test] + fn overlay_labels_match_modes() { + let ratio = 21.0 / 9.0; + assert_eq!( + AspectMode::AutoDetect.overlay_label(ratio), + format!("Auto ({:.2}:1)", ratio) + ); + assert_eq!(AspectMode::Ratio21x9.overlay_label(ratio), "21:9 Ultrawide"); + assert_eq!(AspectMode::Cinema.overlay_label(ratio), "Cinema"); + } +} diff --git a/src/stremio_app/mod.rs b/src/stremio_app/mod.rs index 0301d8b..33fa972 100644 --- a/src/stremio_app/mod.rs +++ b/src/stremio_app/mod.rs @@ -1,5 +1,6 @@ pub mod app; pub use app::MainWindow; +pub mod aspect_ratio; pub mod ipc; pub mod stremio_player; pub mod stremio_server; diff --git a/src/stremio_app/stremio_player/communication.rs b/src/stremio_app/stremio_player/communication.rs index e6d68e5..feb2b99 100644 --- a/src/stremio_app/stremio_player/communication.rs +++ b/src/stremio_app/stremio_player/communication.rs @@ -1,262 +1,267 @@ -use core::convert::TryFrom; -use libmpv2::{events::PropertyData, mpv_end_file_reason, EndFileReason}; -use parse_display::{Display, FromStr}; -use serde::{Deserialize, Serialize}; -use std::fmt; - -// Responses -const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; - -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -pub struct PlayerProprChange { - name: String, - data: serde_json::Value, -} -impl PlayerProprChange { - fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { - match data { - PropertyData::Flag(d) => serde_json::Value::Bool(d), - PropertyData::Int64(d) => serde_json::Value::Number( - serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"), - ), - PropertyData::Double(d) => serde_json::Value::Number( - serde_json::Number::from_f64(d).expect("MPV returned invalid number"), - ), - PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()), - PropertyData::Str(s) => { - if as_json { - serde_json::from_str(s).expect("MPV returned invalid JSON data") - } else { - serde_json::Value::String(s.to_string()) - } - } - PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"), - } - } - pub fn from_name_value(name: String, value: PropertyData) -> Self { - let is_json = JSON_RESPONSES.contains(&name.as_str()); - Self { - name, - data: Self::value_from_format(value, is_json), - } - } -} -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -pub struct PlayerEnded { - reason: String, -} -impl PlayerEnded { - fn string_from_end_reason(data: EndFileReason) -> String { - match data { - mpv_end_file_reason::Error => "error".to_string(), - mpv_end_file_reason::Quit => "quit".to_string(), - _ => "other".to_string(), - } - } - pub fn from_end_reason(data: EndFileReason) -> Self { - Self { - reason: Self::string_from_end_reason(data), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlayerError { - pub error: String, -} -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(untagged)] -pub enum PlayerEvent { - PropChange(PlayerProprChange), - End(PlayerEnded), - Error(PlayerError), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); -impl PlayerResponse<'_> { - pub fn to_value(&self) -> Option { - serde_json::to_value(self).ok() - } -} - -// Player incoming messages from the web UI -/* -Message general case - ["function-name", ["arguments", ...]] -The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. - -["mpv-observe-prop", "prop-name"] -["mpv-set-prop", ["prop-name", prop-val]] -["mpv-command", ["command-name"<, "arguments">]] - -All the function and property names are in kebab-case. - -MPV requires type for any prop-name when observing or setting it's value. -The type for setting is not always the same as the type for observing the prop. - -"mpv-observe-prop" function is the only one that accepts single string -instead of array of arguments - -"mpv-command" function always takes an array even if the command doesn't -have any arguments. For example this are the commands we support: - -["mpv-command", ["loadfile", "file name"]] -["mpv-command", ["stop"]] -*/ -macro_rules! stringable { - ($t:ident) => { - impl From<$t> for String { - fn from(s: $t) -> Self { - s.to_string() - } - } - impl TryFrom for $t { - type Error = parse_display::ParseError; - fn try_from(s: String) -> Result { - s.parse() - } - } - }; -} - -#[allow(clippy::enum_variant_names)] -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum InMsgFn { - MpvSetProp, - MpvCommand, - MpvObserveProp, -} -stringable!(InMsgFn); -// Bool -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum BoolProp { - Pause, - PausedForCache, - Seeking, - EofReached, -} -stringable!(BoolProp); -// Int -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum IntProp { - Aid, - Vid, - Sid, -} -stringable!(IntProp); -// Fp -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum FpProp { - TimePos, - Mute, - Volume, - Duration, - SubDelay, - SubScale, - CacheBufferingState, - SubPos, - Speed, -} -stringable!(FpProp); -// Str -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -pub enum StrProp { - FfmpegVersion, - Hwdec, - InputDefaltBindings, - InputVoKeyboard, - Metadata, - MpvVersion, - Osc, - Path, - SubAssOverride, - SubBackColor, - SubBorderColor, - SubColor, - TrackList, - VideoParams, - Vo, -} -stringable!(StrProp); - -// Any -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(untagged)] -pub enum PropKey { - Bool(BoolProp), - Int(IntProp), - Fp(FpProp), - Str(StrProp), -} -impl fmt::Display for PropKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::Bool(v) => write!(f, "{v}"), - Self::Int(v) => write!(f, "{v}"), - Self::Fp(v) => write!(f, "{v}"), - Self::Str(v) => write!(f, "{v}"), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum PropVal { - Bool(bool), - Str(String), - Num(f64), -} - -#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(try_from = "String", into = "String")] -#[display(style = "kebab-case")] -#[serde(untagged)] -pub enum MpvCmd { - Loadfile, - Stop, -} -stringable!(MpvCmd); - -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] -#[serde(untagged)] -pub enum CmdVal { - Single((MpvCmd,)), - Double(MpvCmd, String), - Tripple(MpvCmd, String, String), - Quadruple(MpvCmd, String, String, String), - Quintuple(MpvCmd, String, String, String, String), -} -impl From for Vec { - fn from(cmd: CmdVal) -> Vec { - match cmd { - CmdVal::Single(cmd) => vec![cmd.0.to_string()], - CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], - CmdVal::Tripple(cmd, arg1, arg2) => vec![cmd.to_string(), arg1, arg2], - CmdVal::Quadruple(cmd, arg1, arg2, arg3) => vec![cmd.to_string(), arg1, arg2, arg3], - CmdVal::Quintuple(cmd, arg1, arg2, arg3, arg4) => { - vec![cmd.to_string(), arg1, arg2, arg3, arg4] - } - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged)] -pub enum InMsgArgs { - StProp(PropKey, PropVal), - Cmd(CmdVal), - ObProp(PropKey), -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct InMsg(pub InMsgFn, pub InMsgArgs); +use core::convert::TryFrom; +use libmpv2::{events::PropertyData, mpv_end_file_reason, EndFileReason}; +use parse_display::{Display, FromStr}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +// Responses +const JSON_RESPONSES: [&str; 3] = ["track-list", "video-params", "metadata"]; + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct PlayerProprChange { + name: String, + data: serde_json::Value, +} +impl PlayerProprChange { + fn value_from_format(data: PropertyData, as_json: bool) -> serde_json::Value { + match data { + PropertyData::Flag(d) => serde_json::Value::Bool(d), + PropertyData::Int64(d) => serde_json::Value::Number( + serde_json::Number::from_f64(d as f64).expect("MPV returned invalid number"), + ), + PropertyData::Double(d) => serde_json::Value::Number( + serde_json::Number::from_f64(d).expect("MPV returned invalid number"), + ), + PropertyData::OsdStr(s) => serde_json::Value::String(s.to_string()), + PropertyData::Str(s) => { + if as_json { + serde_json::from_str(s).expect("MPV returned invalid JSON data") + } else { + serde_json::Value::String(s.to_string()) + } + } + PropertyData::Node(_) => unimplemented!("`PropertyData::Node` is not supported"), + } + } + pub fn from_name_value(name: String, value: PropertyData) -> Self { + let is_json = JSON_RESPONSES.contains(&name.as_str()); + Self { + name, + data: Self::value_from_format(value, is_json), + } + } +} +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct PlayerEnded { + reason: String, +} +impl PlayerEnded { + fn string_from_end_reason(data: EndFileReason) -> String { + match data { + mpv_end_file_reason::Error => "error".to_string(), + mpv_end_file_reason::Quit => "quit".to_string(), + _ => "other".to_string(), + } + } + pub fn from_end_reason(data: EndFileReason) -> Self { + Self { + reason: Self::string_from_end_reason(data), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlayerError { + pub error: String, +} +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum PlayerEvent { + PropChange(PlayerProprChange), + End(PlayerEnded), + Error(PlayerError), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PlayerResponse<'a>(pub &'a str, pub PlayerEvent); +impl PlayerResponse<'_> { + pub fn to_value(&self) -> Option { + serde_json::to_value(self).ok() + } +} + +// Player incoming messages from the web UI +/* +Message general case - ["function-name", ["arguments", ...]] +The function could be either mpv-observe-prop, mpv-set-prop or mpv-command. + +["mpv-observe-prop", "prop-name"] +["mpv-set-prop", ["prop-name", prop-val]] +["mpv-command", ["command-name"<, "arguments">]] + +All the function and property names are in kebab-case. + +MPV requires type for any prop-name when observing or setting it's value. +The type for setting is not always the same as the type for observing the prop. + +"mpv-observe-prop" function is the only one that accepts single string +instead of array of arguments + +"mpv-command" function always takes an array even if the command doesn't +have any arguments. For example this are the commands we support: + +["mpv-command", ["loadfile", "file name"]] +["mpv-command", ["stop"]] +*/ +macro_rules! stringable { + ($t:ident) => { + impl From<$t> for String { + fn from(s: $t) -> Self { + s.to_string() + } + } + impl TryFrom for $t { + type Error = parse_display::ParseError; + fn try_from(s: String) -> Result { + s.parse() + } + } + }; +} + +#[allow(clippy::enum_variant_names)] +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum InMsgFn { + MpvSetProp, + MpvCommand, + MpvObserveProp, +} +stringable!(InMsgFn); +// Bool +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum BoolProp { + Pause, + PausedForCache, + Seeking, + EofReached, + Keepaspect, +} +stringable!(BoolProp); +// Int +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum IntProp { + Aid, + Vid, + Sid, +} +stringable!(IntProp); +// Fp +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum FpProp { + TimePos, + Mute, + Volume, + Duration, + SubDelay, + SubScale, + CacheBufferingState, + SubPos, + Speed, + VideoAspectOverride, + VideoZoom, + Panscan, +} +stringable!(FpProp); +// Str +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +pub enum StrProp { + FfmpegVersion, + Hwdec, + InputDefaltBindings, + InputVoKeyboard, + Metadata, + MpvVersion, + Osc, + Path, + SubAssOverride, + SubBackColor, + SubBorderColor, + SubColor, + TrackList, + VideoParams, + Vo, + VideoUnscaled, +} +stringable!(StrProp); + +// Any +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(untagged)] +pub enum PropKey { + Bool(BoolProp), + Int(IntProp), + Fp(FpProp), + Str(StrProp), +} +impl fmt::Display for PropKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Bool(v) => write!(f, "{v}"), + Self::Int(v) => write!(f, "{v}"), + Self::Fp(v) => write!(f, "{v}"), + Self::Str(v) => write!(f, "{v}"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum PropVal { + Bool(bool), + Str(String), + Num(f64), +} + +#[derive(Display, FromStr, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +#[display(style = "kebab-case")] +#[serde(untagged)] +pub enum MpvCmd { + Loadfile, + Stop, +} +stringable!(MpvCmd); + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(untagged)] +pub enum CmdVal { + Single((MpvCmd,)), + Double(MpvCmd, String), + Tripple(MpvCmd, String, String), + Quadruple(MpvCmd, String, String, String), + Quintuple(MpvCmd, String, String, String, String), +} +impl From for Vec { + fn from(cmd: CmdVal) -> Vec { + match cmd { + CmdVal::Single(cmd) => vec![cmd.0.to_string()], + CmdVal::Double(cmd, arg) => vec![cmd.to_string(), arg], + CmdVal::Tripple(cmd, arg1, arg2) => vec![cmd.to_string(), arg1, arg2], + CmdVal::Quadruple(cmd, arg1, arg2, arg3) => vec![cmd.to_string(), arg1, arg2, arg3], + CmdVal::Quintuple(cmd, arg1, arg2, arg3, arg4) => { + vec![cmd.to_string(), arg1, arg2, arg3, arg4] + } + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum InMsgArgs { + StProp(PropKey, PropVal), + Cmd(CmdVal), + ObProp(PropKey), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InMsg(pub InMsgFn, pub InMsgArgs); diff --git a/src/stremio_app/stremio_player/mod.rs b/src/stremio_app/stremio_player/mod.rs index 06da418..7d4a49b 100644 --- a/src/stremio_app/stremio_player/mod.rs +++ b/src/stremio_app/stremio_player/mod.rs @@ -2,8 +2,8 @@ pub mod player; pub use player::Player; pub mod communication; pub use communication::{ - CmdVal, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, PlayerProprChange, PlayerResponse, - PropKey, PropVal, + BoolProp, CmdVal, FpProp, InMsg, InMsgArgs, InMsgFn, PlayerEnded, PlayerEvent, + PlayerProprChange, PlayerResponse, PropKey, PropVal, StrProp, }; #[cfg(test)] mod communication_tests; diff --git a/src/stremio_app/stremio_wevbiew/wevbiew.rs b/src/stremio_app/stremio_wevbiew/wevbiew.rs index 163f161..6c99da0 100644 --- a/src/stremio_app/stremio_wevbiew/wevbiew.rs +++ b/src/stremio_app/stremio_wevbiew/wevbiew.rs @@ -166,8 +166,72 @@ impl PartialUi for WebView { try{console.log('Shell JS injected');if(window.self === window.top) { window.qt={webChannelTransport:{send:window.chrome.webview.postMessage}}; - window.chrome.webview.addEventListener('message',ev=>window.qt.webChannelTransport.onmessage(ev)); + window.chrome.webview.addEventListener('message',ev=>{ + try{ + const data = typeof ev.data === "string" ? JSON.parse(ev.data) : ev.data; + if(data && data.__borderbreakerOverlay){ + if(window.__bbShowOverlay){ window.__bbShowOverlay(data.__borderbreakerOverlay); } + return; + } + }catch(err){} + window.qt.webChannelTransport.onmessage(ev); + }); }}catch(e){} + try{ + if(!document.getElementById('bb-overlay-style')){ + const style = document.createElement('style'); + style.id = 'bb-overlay-style'; + style.textContent = ` + #bbOverlayToast { + position: fixed; + top: 28px; + left: 50%; + transform: translateX(-50%) translateY(-8px); + padding: 10px 20px; + border-radius: 10px; + background: rgba(13, 15, 23, 0.92); + color: #fff; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.2px; + box-shadow: 0 12px 35px rgba(0,0,0,0.4); + z-index: 99999; + opacity: 0; + pointer-events: none; + transition: opacity .18s ease, transform .18s ease; + font-family: 'Segoe UI', 'Inter', sans-serif; + } + #bbOverlayToast.visible { + opacity: 1; + transform: translateX(-50%) translateY(0); + } + `; + document.head.appendChild(style); + const toast = document.createElement('div'); + toast.id = 'bbOverlayToast'; + document.body.appendChild(toast); + } + window.__bbShowOverlay = (text) => { + const toast = document.getElementById('bbOverlayToast'); + if(!toast) return; + toast.textContent = text; + toast.classList.add('visible'); + clearTimeout(window.__bbOverlayTimer); + window.__bbOverlayTimer = setTimeout(() => toast.classList.remove('visible'), 1700); + }; + }catch(e){} + try{ + window.addEventListener('keydown', (e) => { + const tag = (e.target && e.target.tagName || "").toUpperCase(); + if(e.ctrlKey || e.altKey || e.metaKey) return; + if(tag === 'INPUT' || tag === 'TEXTAREA') return; + if(e.target && e.target.isContentEditable) return; + if((e.key || "").toLowerCase() === "b") { + e.preventDefault(); + window.chrome.webview.postMessage('{"id":1,"args":["borderbreaker-cycle"]}'); + } + }, true); + }catch(e){} "##, |_| Ok(())).expect("Cannot add script to webview"); Ok(()) }).expect("Cannot add content loading"); @@ -177,16 +241,17 @@ impl PartialUi for WebView { controller .move_focus(webview2::MoveFocusReason::Programmatic) .ok(); - controller.add_accelerator_key_pressed(move |_, e| { - // Block F7, Ctrl+F, and Ctrl+G - let k = e.get_virtual_key()?; - if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F { - e.put_handled(true) - } else { - Ok(()) - } - }) - .unwrap(); + controller + .add_accelerator_key_pressed(move |_, e| { + // Block F7, Ctrl+F, and Ctrl+G + let k = e.get_virtual_key()?; + if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F { + e.put_handled(true) + } else { + Ok(()) + } + }) + .unwrap(); controller_clone .set(controller) diff --git a/src/stremio_app/window_helper.rs b/src/stremio_app/window_helper.rs index 68c5547..f164125 100644 --- a/src/stremio_app/window_helper.rs +++ b/src/stremio_app/window_helper.rs @@ -154,3 +154,13 @@ impl WindowStyle { } } } + +pub fn primary_monitor_ratio() -> f32 { + let width = unsafe { GetSystemMetrics(SM_CXSCREEN) } as f32; + let height = unsafe { GetSystemMetrics(SM_CYSCREEN) } as f32; + if height <= f32::EPSILON { + 0.0 + } else { + width / height + } +}