mirror of
https://github.com/Stremio/stremio-shell-ng.git
synced 2026-03-11 21:27:06 +00:00
Merge 1ae83be225 into 81fa5c902d
This commit is contained in:
commit
3c7a18b75c
9 changed files with 915 additions and 504 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
/target
|
||||
/stremio*.exe
|
||||
engine.log
|
||||
engine.err
|
||||
/target
|
||||
/stremio*.exe
|
||||
/libmpv-2.dll
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<AspectController>,
|
||||
pub aspect_player_tx: RefCell<Option<flume::Sender<String>>>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
286
src/stremio_app/aspect_ratio.rs
Normal file
286
src/stremio_app/aspect_ratio.rs
Normal file
|
|
@ -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<PathBuf> = 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<f64>,
|
||||
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<AspectMode>,
|
||||
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<String>) {
|
||||
self.apply_mode(player_tx, self.current_mode());
|
||||
}
|
||||
|
||||
pub fn apply_mode(&self, player_tx: &Sender<String>, 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<AspectConfig> {
|
||||
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<String>, 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<String>, 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<String>, 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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::Value> {
|
||||
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<String> for $t {
|
||||
type Error = parse_display::ParseError;
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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<CmdVal> for Vec<String> {
|
||||
fn from(cmd: CmdVal) -> Vec<String> {
|
||||
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::Value> {
|
||||
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<String> for $t {
|
||||
type Error = parse_display::ParseError;
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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<CmdVal> for Vec<String> {
|
||||
fn from(cmd: CmdVal) -> Vec<String> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue