mirror of
https://github.com/an-anime-team/sleepy-launcher.git
synced 2025-04-03 15:33:28 +03:00
relm4 init commit
This commit is contained in:
parent
da37ea2103
commit
d6b5eb6411
87 changed files with 1333 additions and 8854 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,6 +1,2 @@
|
||||||
/target
|
/target
|
||||||
/assets/ui/.dist
|
/assets/locales/TODO.*
|
||||||
|
|
||||||
/scripts/builds
|
|
||||||
/scripts/appimage/dist
|
|
||||||
/scripts/appimage/*.AppImage
|
|
||||||
|
|
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -1,9 +1,3 @@
|
||||||
[submodule "blueprint-compiler"]
|
[submodule "anime-launcher-sdk"]
|
||||||
path = blueprint-compiler
|
path = anime-launcher-sdk
|
||||||
url = https://gitlab.gnome.org/jwestman/blueprint-compiler
|
url = https://github.com/an-anime-team/anime-launcher-sdk
|
||||||
[submodule "anime-game-core"]
|
|
||||||
path = anime-game-core
|
|
||||||
url = https://github.com/an-anime-team/anime-game-core
|
|
||||||
[submodule "components"]
|
|
||||||
path = components
|
|
||||||
url = https://github.com/an-anime-team/components
|
|
||||||
|
|
1586
Cargo.lock
generated
1586
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "anime-game-launcher"
|
name = "anime-game-launcher"
|
||||||
version = "1.2.4"
|
version = "2.0.0"
|
||||||
description = "Anime Game launcher"
|
description = "Anime Game launcher"
|
||||||
authors = ["Nikita Podvirnyy <suimin.tu.mu.ga.mi@gmail.com>"]
|
authors = ["Nikita Podvirnyy <suimin.tu.mu.ga.mi@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
|
@ -16,19 +16,16 @@ opt-level = 3
|
||||||
glib-build-tools = "0.16"
|
glib-build-tools = "0.16"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
relm4 = { version = "0.5.0-rc.1", features = ["macros", "libadwaita"] }
|
||||||
gtk = { package = "gtk4", version = "0.5", features = ["v4_8"] }
|
gtk = { package = "gtk4", version = "0.5", features = ["v4_8"] }
|
||||||
adw = { package = "libadwaita", version = "0.2", features = ["v1_2"] }
|
adw = { package = "libadwaita", version = "0.2", features = ["v1_2"] }
|
||||||
rfd = { version = "0.10", features = ["xdg-portal"], default-features = false }
|
|
||||||
|
|
||||||
anime-game-core = { path = "anime-game-core", features = ["all", "static", "genshin"] }
|
anime-launcher-sdk = { path = "anime-launcher-sdk" }
|
||||||
wincompatlib = { version = "0.1.3", features = ["dxvk"] }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
tracing = "0.1"
|
||||||
serde_json = "1.0"
|
tracing-subscriber = "0.3"
|
||||||
|
|
||||||
|
fluent-templates = "0.8"
|
||||||
|
unic-langid = "0.9"
|
||||||
|
|
||||||
dirs = "4.0.0"
|
|
||||||
wait_not_await = "0.2.1"
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
anyhow = "1.0"
|
|
||||||
md5 = "0.7"
|
|
||||||
cached = { version = "0.41", features = ["proc_macro"] }
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit ea02d1c482da9701f4a5517f05e3e2ad68b71295
|
|
1
anime-launcher-sdk
Submodule
1
anime-launcher-sdk
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f85152c83a715957b553e5fb1f2c6b2da93f2f89
|
|
@ -1,7 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Name=An Anime Game Launcher GTK
|
|
||||||
Icon=icon
|
|
||||||
Exec=AppRun
|
|
||||||
Type=Application
|
|
||||||
Categories=Game
|
|
||||||
Terminal=false
|
|
61
assets/locales/en/main.ftl
Normal file
61
assets/locales/en/main.ftl
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
custom = Custom
|
||||||
|
none = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
launch = Launch
|
||||||
|
|
||||||
|
preferences = Preferences
|
||||||
|
|
||||||
|
general = General
|
||||||
|
|
||||||
|
launcher-language = Launcher language
|
||||||
|
|
||||||
|
game-voiceovers = Game voiceovers
|
||||||
|
english = English
|
||||||
|
japanese = Japanese
|
||||||
|
korean = Korean
|
||||||
|
chinese = Chinese
|
||||||
|
|
||||||
|
repair-game = Repair game
|
||||||
|
|
||||||
|
status = Status
|
||||||
|
|
||||||
|
game-version = Game version
|
||||||
|
patch-version = Patch version
|
||||||
|
|
||||||
|
enhancements = Enhancements
|
||||||
|
|
||||||
|
wine = Wine
|
||||||
|
|
||||||
|
synchronization = Synchronization
|
||||||
|
wine-sync-description = Technology used to synchronize inner wine events
|
||||||
|
|
||||||
|
language = Language
|
||||||
|
wine-lang-description = Language used in the wine environment. Can fix keyboard layout issues
|
||||||
|
system = System
|
||||||
|
|
||||||
|
borderless-window = Borderless window
|
||||||
|
virtual-desktop = Virtual desktop
|
||||||
|
|
||||||
|
game = Game
|
||||||
|
|
||||||
|
hud = HUD
|
||||||
|
|
||||||
|
fsr = FSR
|
||||||
|
fsr-description = Upscales game to your monitor size. To use select lower resolution in the game's settings and press Alt+Enter
|
||||||
|
ultra-quality = Ultra quality
|
||||||
|
quality = Quality
|
||||||
|
balanced = Balanced
|
||||||
|
performance = Performance
|
||||||
|
|
||||||
|
gamemode = Gamemode
|
||||||
|
gamemode-description = Prioritize the game over the rest of the processes
|
||||||
|
|
||||||
|
fps-unlocker = FPS Unlocker
|
||||||
|
|
||||||
|
enabled = Enabled
|
||||||
|
fps-unlocker-description = Remove frames rendering limitation by modifying the game's memory. Can be detected by the anti-cheat
|
||||||
|
|
||||||
|
power-saving = Power saving
|
||||||
|
power-saving-description = Automatically set the FPS limit to 10 and low process priority upon losing focus to the game (e.g. tabbing out)
|
61
assets/locales/ru/main.ftl
Normal file
61
assets/locales/ru/main.ftl
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
custom = Свое значение
|
||||||
|
none = Нет
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
launch = Запустить
|
||||||
|
|
||||||
|
preferences = Настройки
|
||||||
|
|
||||||
|
general = Основное
|
||||||
|
|
||||||
|
launcher-language = Язык лаунчера
|
||||||
|
|
||||||
|
game-voiceovers = Язык озвучки
|
||||||
|
english = Английский
|
||||||
|
japanese = Японский
|
||||||
|
korean = Корейский
|
||||||
|
chinese = Китайский
|
||||||
|
|
||||||
|
repair-game = Починить игру
|
||||||
|
|
||||||
|
status = Статус
|
||||||
|
|
||||||
|
game-version = Версия игры
|
||||||
|
patch-version = Версия патча
|
||||||
|
|
||||||
|
enhancements = Улучшения
|
||||||
|
|
||||||
|
wine = Wine
|
||||||
|
|
||||||
|
synchronization = Синхронизация
|
||||||
|
wine-sync-description = Технология, используемая для синхронизации внутренних событий Wine
|
||||||
|
|
||||||
|
language = Язык
|
||||||
|
wine-lang-description = Язык, используемый в окружении Wine. Может исправить проблемы с раскладкой клавиатуры
|
||||||
|
system = Системный
|
||||||
|
|
||||||
|
borderless-window = Окно без рамок
|
||||||
|
virtual-desktop = Виртуальный рабочий стол
|
||||||
|
|
||||||
|
game = Игра
|
||||||
|
|
||||||
|
hud = HUD
|
||||||
|
|
||||||
|
fsr = FSR
|
||||||
|
fsr-description = Для использования установите меньшее разрешение в настройках игры и нажмите Alt+Enter
|
||||||
|
ultra-quality = Ультра
|
||||||
|
quality = Хорошо
|
||||||
|
balanced = Сбалансированно
|
||||||
|
performance = Производительно
|
||||||
|
|
||||||
|
gamemode = Gamemode
|
||||||
|
gamemode-description = Выделять игре приоритет перед остальными процессами
|
||||||
|
|
||||||
|
fps-unlocker = FPS Unlocker
|
||||||
|
|
||||||
|
enabled = Включен
|
||||||
|
fps-unlocker-description = Убрать ограничение количества кадров модифицируя память игры. Может быть обнаружено античитом
|
||||||
|
|
||||||
|
power-saving = Энергосбережение
|
||||||
|
power-saving-description = Автоматически устанавливать предел количества кадров до 10 и снижать приоритет процесса игры когда она не находится в фокусе
|
|
@ -1,26 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/org/app/assets">
|
<gresource prefix="/org/app">
|
||||||
<file compressed="true">images/icon.png</file>
|
<file compressed="true">images/icon.png</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
<gresource prefix="/org/app/ui">
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="main.ui">ui/.dist/main.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="first_run.ui">ui/.dist/first_run.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="preferences.ui">ui/.dist/preferences.ui</file>
|
|
||||||
</gresource>
|
|
||||||
<gresource prefix="/org/app/ui/first_run">
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="welcome.ui">ui/.dist/first_run/welcome.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="dependencies.ui">ui/.dist/first_run/dependencies.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="tos_warning.ui">ui/.dist/first_run/tos_warning.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="default_paths.ui">ui/.dist/first_run/default_paths.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="voice_packages.ui">ui/.dist/first_run/voice_packages.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="download_components.ui">ui/.dist/first_run/download_components.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="finish.ui">ui/.dist/first_run/finish.ui</file>
|
|
||||||
</gresource>
|
|
||||||
<gresource prefix="/org/app/ui/preferences">
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="general.ui">ui/.dist/preferences/general.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="enhancements.ui">ui/.dist/preferences/enhancements.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="gamescope.ui">ui/.dist/preferences/gamescope.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks" compressed="true" alias="environment.ui">ui/.dist/preferences/environment.ui</file>
|
|
||||||
</gresource>
|
|
||||||
</gresources>
|
</gresources>
|
|
@ -1,3 +0,0 @@
|
||||||
progressbar > text {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Adw.ApplicationWindow window {
|
|
||||||
default-width: 780;
|
|
||||||
default-height: 560;
|
|
||||||
|
|
||||||
content: Adw.ToastOverlay toast_overlay {
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: vertical;
|
|
||||||
|
|
||||||
Adw.HeaderBar {
|
|
||||||
title-widget: Adw.WindowTitle {
|
|
||||||
title: "An Anime Game Launcher";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.Carousel carousel {
|
|
||||||
allow-mouse-drag: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.CarouselIndicatorDots {
|
|
||||||
carousel: carousel;
|
|
||||||
height-request: 32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Set default paths";
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Adw.ActionRow runners_folder {
|
|
||||||
title: "Runners saving folder";
|
|
||||||
activatable: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow dxvk_folder {
|
|
||||||
title: "DXVK saving folder";
|
|
||||||
activatable: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow prefix_folder {
|
|
||||||
title: "Wine prefix folder";
|
|
||||||
activatable: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow game_folder {
|
|
||||||
title: "Game installation folder";
|
|
||||||
activatable: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow patch_folder {
|
|
||||||
title: "Patch storing folder";
|
|
||||||
activatable: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow temp_folder {
|
|
||||||
title: "Temp data saving folder";
|
|
||||||
activatable: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button continue_button {
|
|
||||||
label: "Continue";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button exit_button {
|
|
||||||
label: "Exit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Label {
|
|
||||||
label: "You're missing some dependencies!";
|
|
||||||
margin-top: 32;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "You must install some packages to your system before continue installation process";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
justify: center;
|
|
||||||
margin-top: 32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: vertical;
|
|
||||||
spacing: 16;
|
|
||||||
|
|
||||||
Gtk.Box pkg_pacman {
|
|
||||||
orientation: vertical;
|
|
||||||
spacing: 16;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Arch (pacman)";
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Entry {
|
|
||||||
text: "sudo pacman -Syu git xdelta3";
|
|
||||||
editable: false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Box pkg_apt {
|
|
||||||
orientation: vertical;
|
|
||||||
spacing: 16;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Debian / Ubuntu (apt)";
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Entry {
|
|
||||||
text: "sudo apt install git xdelta3";
|
|
||||||
editable: false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Box pkg_dnf {
|
|
||||||
orientation: vertical;
|
|
||||||
spacing: 16;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Fedora (dnf)";
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Entry {
|
|
||||||
text: "sudo dnf install git xdelta";
|
|
||||||
editable: false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button check_button {
|
|
||||||
label: "Check";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button exit_button {
|
|
||||||
label: "Exit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Download default components";
|
|
||||||
margin-top: 16;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Adw.ComboRow wine_version {
|
|
||||||
title: "Wine version";
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ComboRow dxvk_version {
|
|
||||||
title: "DXVK version";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup buttons_group {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button download_button {
|
|
||||||
label: "Download";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button exit_button {
|
|
||||||
label: "Exit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup progress_bar_group {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
halign: center;
|
|
||||||
margin-top: 64;
|
|
||||||
spacing: 20;
|
|
||||||
|
|
||||||
Gtk.ProgressBar progress_bar {
|
|
||||||
text: "Downloading: 37% (3.7 of 10 GB)";
|
|
||||||
show-text: true;
|
|
||||||
|
|
||||||
width-request: 360;
|
|
||||||
fraction: 0.37;
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button {
|
|
||||||
label: "Pause";
|
|
||||||
sensitive: false;
|
|
||||||
tooltip-text: "Work in progress";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Downloading finished!";
|
|
||||||
margin-top: 64;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "All the basic components were downloaded. Now you can restart the launcher and download the game. Welcome to our club!";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
justify: center;
|
|
||||||
margin-top: 32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button restart_button {
|
|
||||||
label: "Restart";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button exit_button {
|
|
||||||
label: "Exit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Label {
|
|
||||||
label: "ToS violation warning";
|
|
||||||
margin-top: 8;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: vertical;
|
|
||||||
margin-top: 32;
|
|
||||||
spacing: 12;
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "This launcher is an unofficial tool, in no way related to miHoYo nor COGNOSPHERE.";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "This tool is designed to facilitate playing Genshin Impact on Linux, and was built with the sole purpose of installing and running the game with less hassle.";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "It does so by using existing components and making the experience simple for the user.";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "However, some components used here likely break the miHoYo Terms of Service for Genshin Impact.";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "If you are using this launcher, your player account could become identified as TOS-non-compliant by miHoYo/COGNOSPHERE.";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "If this happens, as your account would be disobeying TOS, miHoYo/COGNOSPHERE are free to do what they want. Including banning.";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "If you understand the risk of trying to play the game in an unofficial capacity, press OK and let's go researching the world of Teyvat!";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button continue_button {
|
|
||||||
label: "Continue";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button exit_button {
|
|
||||||
label: "Exit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Select voice packages";
|
|
||||||
margin-top: 16;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup voice_packages_group {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button continue_button {
|
|
||||||
label: "Continue";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button exit_button {
|
|
||||||
label: "Exit";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box page {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Image {
|
|
||||||
resource: "/org/app/assets/images/icon.png";
|
|
||||||
|
|
||||||
vexpand: true;
|
|
||||||
margin-top: 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "An Anime Game Launcher";
|
|
||||||
margin-top: 32;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "Hi there! Welcome to the An Anime Game Launcher. We need to prepare some stuff and download default components before you could run the game";
|
|
||||||
|
|
||||||
wrap: true;
|
|
||||||
justify: center;
|
|
||||||
margin-top: 32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Gtk.Button continue_button {
|
|
||||||
label: "Continue";
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button advanced_button {
|
|
||||||
label: "Advanced";
|
|
||||||
tooltip-text: "You can choose default folders paths";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Adw.ApplicationWindow window {
|
|
||||||
default-width: 900;
|
|
||||||
default-height: 600;
|
|
||||||
|
|
||||||
content: Adw.ToastOverlay toast_overlay {
|
|
||||||
Adw.Leaflet leaflet {
|
|
||||||
can-navigate-back: true;
|
|
||||||
can-unfold: false;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: vertical;
|
|
||||||
hexpand: true;
|
|
||||||
|
|
||||||
Adw.HeaderBar {
|
|
||||||
title-widget: Adw.WindowTitle {
|
|
||||||
title: "An Anime Game Launcher";
|
|
||||||
};
|
|
||||||
|
|
||||||
[end]
|
|
||||||
Gtk.MenuButton menu {
|
|
||||||
menu-model: app_menu;
|
|
||||||
icon-name: "open-menu-symbolic";
|
|
||||||
halign: end;
|
|
||||||
valign: center;
|
|
||||||
margin-start: 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.StatusPage status_page {
|
|
||||||
icon-name: "image-loading-symbolic";
|
|
||||||
title: "Loading data";
|
|
||||||
|
|
||||||
vexpand: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesPage launcher_content {
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
Gtk.Image icon {
|
|
||||||
resource: "/org/app/assets/images/icon.png";
|
|
||||||
|
|
||||||
vexpand: true;
|
|
||||||
margin-top: 48;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Label {
|
|
||||||
label: "An Anime Game Launcher";
|
|
||||||
margin-top: 32;
|
|
||||||
|
|
||||||
styles ["title-1"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup launch_game_group {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
halign: center;
|
|
||||||
margin-top: 64;
|
|
||||||
spacing: 8;
|
|
||||||
|
|
||||||
Gtk.Button predownload_game {
|
|
||||||
icon-name: "document-save-symbolic";
|
|
||||||
tooltip-text: "Pre-download 3.1.0 update (15 GB)";
|
|
||||||
|
|
||||||
hexpand: false;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
styles ["warning"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button launch_game {
|
|
||||||
label: "Launch";
|
|
||||||
|
|
||||||
hexpand: false;
|
|
||||||
width-request: 200;
|
|
||||||
|
|
||||||
styles ["suggested-action"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button open_preferences {
|
|
||||||
icon-name: "emblem-system-symbolic";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup progress_bar_group {
|
|
||||||
vexpand: true;
|
|
||||||
valign: center;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
halign: center;
|
|
||||||
margin-top: 64;
|
|
||||||
spacing: 20;
|
|
||||||
|
|
||||||
Gtk.ProgressBar progress_bar {
|
|
||||||
show-text: true;
|
|
||||||
width-request: 360;
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button {
|
|
||||||
label: "Pause";
|
|
||||||
sensitive: false;
|
|
||||||
tooltip-text: "Work in progress";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.AboutWindow about {
|
|
||||||
application-name: "An Anime Game Launcher";
|
|
||||||
application-icon: "moe.launcher.an-anime-game-launcher-gtk";
|
|
||||||
|
|
||||||
website: "https://github.com/an-anime-team/an-anime-game-launcher-gtk";
|
|
||||||
issue-url: "https://github.com/an-anime-team/an-anime-game-launcher-gtk/issues";
|
|
||||||
|
|
||||||
modal: true;
|
|
||||||
transient-for: window;
|
|
||||||
hide-on-close: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
menu app_menu {
|
|
||||||
section {
|
|
||||||
submenu {
|
|
||||||
label: "Open";
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: "Launcher folder";
|
|
||||||
action: "open-launcher-folder.open-launcher-folder";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: "Game folder";
|
|
||||||
action: "open-game-folder.open-game-folder";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: "Config file";
|
|
||||||
action: "open-config-file.open-config-file";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
item ("About", "show-about-dialog.show-about-dialog")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Gtk.Box preferences {
|
|
||||||
orientation: vertical;
|
|
||||||
|
|
||||||
Adw.HeaderBar {
|
|
||||||
title-widget: Adw.WindowTitle {
|
|
||||||
title: "Preferences";
|
|
||||||
};
|
|
||||||
|
|
||||||
Gtk.Button preferences_go_back {
|
|
||||||
icon-name: "go-previous-symbolic";
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.StatusPage status_page {
|
|
||||||
icon-name: "image-loading-symbolic";
|
|
||||||
title: "Loading data";
|
|
||||||
|
|
||||||
vexpand: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.Flap flap {
|
|
||||||
vexpand: true;
|
|
||||||
visible: false;
|
|
||||||
|
|
||||||
flap: Gtk.StackSidebar {
|
|
||||||
width-request: 200;
|
|
||||||
stack: stack;
|
|
||||||
};
|
|
||||||
|
|
||||||
content: Gtk.Stack stack {};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Adw.PreferencesPage page {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Wine";
|
|
||||||
|
|
||||||
Adw.ComboRow sync_combo {
|
|
||||||
title: "Synchronization";
|
|
||||||
subtitle: "Technology used to synchronize inner wine events";
|
|
||||||
|
|
||||||
model: Gtk.StringList {
|
|
||||||
strings [
|
|
||||||
"None",
|
|
||||||
"ESync",
|
|
||||||
"FSync",
|
|
||||||
"Futex2"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ComboRow wine_lang {
|
|
||||||
title: "Language";
|
|
||||||
subtitle: "Choose the language to use in wine environment. Can fix keyboard layout detection in-game";
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Borderless window";
|
|
||||||
|
|
||||||
Gtk.Switch borderless {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ComboRow virtual_desktop_row {
|
|
||||||
title: "Virtual desktop";
|
|
||||||
|
|
||||||
Gtk.Switch virtual_desktop {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Game";
|
|
||||||
|
|
||||||
Adw.ComboRow hud_combo {
|
|
||||||
title: "HUD";
|
|
||||||
|
|
||||||
model: Gtk.StringList {
|
|
||||||
strings [
|
|
||||||
"None",
|
|
||||||
"DXVK",
|
|
||||||
"MangoHUD"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ComboRow fsr_combo {
|
|
||||||
title: "FSR";
|
|
||||||
subtitle: "Upscales game to your monitor size. To use select lower\nresolution in the game's settings and press Alt+Enter";
|
|
||||||
|
|
||||||
model: Gtk.StringList {
|
|
||||||
strings [
|
|
||||||
"Ultra Quality",
|
|
||||||
"Quality",
|
|
||||||
"Balanced",
|
|
||||||
"Performance"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
Gtk.Switch fsr_switcher {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow gamemode_row {
|
|
||||||
title: "Gamemode";
|
|
||||||
subtitle: "This prioritizes the game over the rest of the processes";
|
|
||||||
|
|
||||||
Gtk.Switch gamemode_switcher {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow gamescope_row {
|
|
||||||
title: "Gamescope";
|
|
||||||
subtitle: "Gamescope is a tool from Valve that allows for games to run in an isolated Xwayland instance and supports AMD, Intel, and Nvidia GPUs";
|
|
||||||
|
|
||||||
Gtk.Button gamescope_settings {
|
|
||||||
icon-name: "emblem-system-symbolic";
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
styles ["flat"]
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Switch gamescope_switcher {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "FPS Unlocker";
|
|
||||||
|
|
||||||
Adw.ComboRow fps_unlocker_combo {
|
|
||||||
title: "Enabled";
|
|
||||||
subtitle: "Remove frames rendering limitation modifying the game's memory. Can be detected by the anti-cheat";
|
|
||||||
|
|
||||||
Gtk.Switch fps_unlocker_switcher {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Power saving";
|
|
||||||
subtitle: "Automatically sets the fps limit to 10 and low process priority upon losing focus to the game (e.g. tabbing out of the game)";
|
|
||||||
|
|
||||||
Gtk.Switch fps_unlocker_power_saving_switcher {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Monitor";
|
|
||||||
subtitle: "Number of monitor you want to run the game on";
|
|
||||||
|
|
||||||
Gtk.SpinButton fps_unlocker_monitor_num {
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
adjustment: Gtk.Adjustment {
|
|
||||||
value: 1;
|
|
||||||
lower: 1;
|
|
||||||
upper: 10;
|
|
||||||
page-increment: 1;
|
|
||||||
step-increment: 1;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ComboRow fps_unlocker_window_mode_combo {
|
|
||||||
title: "Window mode";
|
|
||||||
|
|
||||||
model: Gtk.StringList {
|
|
||||||
strings [
|
|
||||||
"Default",
|
|
||||||
"Popup",
|
|
||||||
"Fullscreen"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ComboRow fps_unlocker_priority_combo {
|
|
||||||
title: "Priority";
|
|
||||||
subtitle: "Game process priority";
|
|
||||||
|
|
||||||
model: Gtk.StringList {
|
|
||||||
strings [
|
|
||||||
"Realtime",
|
|
||||||
"High",
|
|
||||||
"Above normal",
|
|
||||||
"Normal",
|
|
||||||
"Below normal",
|
|
||||||
"Low"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Adw.PreferencesPage page {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Game command";
|
|
||||||
|
|
||||||
Gtk.Entry command {
|
|
||||||
placeholder-text: "%command%";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "New variable";
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
|
|
||||||
Gtk.Entry name {
|
|
||||||
placeholder-text: "Name";
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Entry value {
|
|
||||||
placeholder-text: "Value";
|
|
||||||
hexpand: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Button add {
|
|
||||||
label: "Add";
|
|
||||||
|
|
||||||
margin-top: 8;
|
|
||||||
halign: start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup variables {
|
|
||||||
title: "Variables";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Adw.PreferencesWindow window {
|
|
||||||
title: "Gamescope";
|
|
||||||
|
|
||||||
modal: true;
|
|
||||||
hide-on-close: true;
|
|
||||||
|
|
||||||
Adw.PreferencesPage {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Game resolution";
|
|
||||||
|
|
||||||
Adw.EntryRow game_width {
|
|
||||||
title: "Width";
|
|
||||||
|
|
||||||
input-purpose: digits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.EntryRow game_height {
|
|
||||||
title: "Height";
|
|
||||||
|
|
||||||
input-purpose: digits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Gamescope resolution";
|
|
||||||
|
|
||||||
Adw.EntryRow gamescope_width {
|
|
||||||
title: "Width";
|
|
||||||
|
|
||||||
input-purpose: digits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.EntryRow gamescope_height {
|
|
||||||
title: "Height";
|
|
||||||
|
|
||||||
input-purpose: digits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Other settings";
|
|
||||||
|
|
||||||
Adw.EntryRow framerate_limit {
|
|
||||||
title: "Framerate limit";
|
|
||||||
|
|
||||||
input-purpose: digits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.EntryRow framerate_unfocused_limit {
|
|
||||||
title: "Unfocused framerate limit";
|
|
||||||
|
|
||||||
input-purpose: digits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Integer scaling";
|
|
||||||
|
|
||||||
Gtk.Switch integer_scaling {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "FSR";
|
|
||||||
|
|
||||||
Gtk.Switch fsr {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Nvidia Image Scaling";
|
|
||||||
|
|
||||||
Gtk.Switch nis {
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Window type";
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
|
|
||||||
Gtk.ToggleButton borderless {
|
|
||||||
label: "Borderless";
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.ToggleButton fullscreen {
|
|
||||||
label: "Fullscreen";
|
|
||||||
valign: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
styles ["linked"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
using Gtk 4.0;
|
|
||||||
using Adw 1;
|
|
||||||
|
|
||||||
Adw.PreferencesPage page {
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "General";
|
|
||||||
|
|
||||||
Adw.ComboRow {
|
|
||||||
title: "Launcher language";
|
|
||||||
|
|
||||||
model: Gtk.StringList {
|
|
||||||
strings [
|
|
||||||
"English",
|
|
||||||
"German",
|
|
||||||
"Russian",
|
|
||||||
"French",
|
|
||||||
"Chinese"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
sensitive: false;
|
|
||||||
tooltip-text: "Work in progress";
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ExpanderRow voiceovers_row {
|
|
||||||
title: "Game voiceovers";
|
|
||||||
subtitle: "Select voice packages used in game";
|
|
||||||
}
|
|
||||||
|
|
||||||
Gtk.Box {
|
|
||||||
orientation: horizontal;
|
|
||||||
spacing: 8;
|
|
||||||
margin-top: 16;
|
|
||||||
|
|
||||||
Gtk.Button repair_game {
|
|
||||||
label: "Repair game";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Status";
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Game version";
|
|
||||||
|
|
||||||
Gtk.Label game_version {
|
|
||||||
label: "2.7.0";
|
|
||||||
|
|
||||||
styles ["success"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Patch version";
|
|
||||||
|
|
||||||
Gtk.Label patch_version {
|
|
||||||
label: "2.7.0";
|
|
||||||
|
|
||||||
styles ["success"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "Wine version";
|
|
||||||
|
|
||||||
Adw.ComboRow wine_selected {
|
|
||||||
title: "Selected version";
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Recommended only";
|
|
||||||
subtitle: "Show only recommended wine versions";
|
|
||||||
|
|
||||||
Gtk.Switch wine_recommended_only {
|
|
||||||
valign: center;
|
|
||||||
state: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup wine_groups {}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup {
|
|
||||||
title: "DXVK version";
|
|
||||||
|
|
||||||
Adw.ComboRow dxvk_selected {
|
|
||||||
title: "Selected version";
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.ActionRow {
|
|
||||||
title: "Recommended only";
|
|
||||||
subtitle: "Show only recommended DXVK versions";
|
|
||||||
|
|
||||||
Gtk.Switch dxvk_recommended_only {
|
|
||||||
valign: center;
|
|
||||||
state: true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Adw.PreferencesGroup dxvk_groups {}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 039d88ab45001cf799c421e58d4669a0596c4d29
|
|
79
build.rs
79
build.rs
|
@ -1,84 +1,7 @@
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::fs::{self, read_dir, create_dir_all, read_to_string};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
fn compile_blueprint<T: ToString>(path: T) -> Result<String, String> {
|
|
||||||
// python blueprint-compiler/blueprint-compiler.py compile ui/main.blp
|
|
||||||
let output = Command::new("python3")
|
|
||||||
.arg("blueprint-compiler/blueprint-compiler.py")
|
|
||||||
.arg("compile")
|
|
||||||
.arg(path.to_string())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(output) => {
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(String::from_utf8(output.stdout).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
Err(String::from_utf8(output.stdout).unwrap())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => Err(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn blp_process_dir(dir: String) {
|
|
||||||
let source_dir = format!("assets/ui/{}", &dir).replace("//", "/");
|
|
||||||
let dist_dir = format!("assets/ui/.dist/{}", &dir).replace("//", "/");
|
|
||||||
|
|
||||||
if let Ok(entries) = read_dir(&source_dir) {
|
|
||||||
if read_dir(&dist_dir).is_err() {
|
|
||||||
create_dir_all(&dist_dir).expect("UI dist dir couldn't be created");
|
|
||||||
}
|
|
||||||
|
|
||||||
// println!("cargo:rerun-if-changed={}/*.blp", &source_dir);
|
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
if let Ok(metadata) = entry.metadata() {
|
|
||||||
let entry_path = entry.path().to_str().unwrap().to_string();
|
|
||||||
let entry_filename = entry.file_name().to_str().unwrap().to_string();
|
|
||||||
|
|
||||||
if metadata.is_file() {
|
|
||||||
let entry_dist_path = format!("{}/{}.ui", &dist_dir, &entry_filename[..entry_filename.len() - 4]);
|
|
||||||
|
|
||||||
match compile_blueprint(&entry_path) {
|
|
||||||
Ok(xml) => {
|
|
||||||
let result = fs::write(entry_dist_path, xml);
|
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
println!("cargo:warning=Couldn't write compiled XML UI: {}", err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
if Path::new(&entry_dist_path).exists() {
|
|
||||||
fs::remove_file(entry_dist_path).expect("Couldn't remove broken file");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("cargo:warning=Couldn't compile {}: {}", entry_path, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if metadata.is_dir() && &entry_filename[0..1] != "." {
|
|
||||||
blp_process_dir(format!("{}/{}", &dir, &entry_filename));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
blp_process_dir(String::new());
|
|
||||||
|
|
||||||
if read_to_string("assets/resources.xml").is_ok() {
|
|
||||||
glib_build_tools::compile_resources(
|
glib_build_tools::compile_resources(
|
||||||
"assets",
|
"assets",
|
||||||
"assets/resources.xml",
|
"assets/resources.xml",
|
||||||
".assets.gresource",
|
"resources.gresource",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 5580f7be0fbdfba677ec32b2fd7d11cb762edebf
|
|
19
src/i18n.rs
Normal file
19
src/i18n.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use fluent_templates::Loader;
|
||||||
|
use unic_langid::{langid, LanguageIdentifier};
|
||||||
|
|
||||||
|
fluent_templates::static_loader! {
|
||||||
|
static LOCALES = {
|
||||||
|
locales: "./assets/locales",
|
||||||
|
fallback_language: "en"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static mut LANG: LanguageIdentifier = langid!("en");
|
||||||
|
|
||||||
|
pub fn tr(id: &str) -> String {
|
||||||
|
unsafe {
|
||||||
|
LOCALES
|
||||||
|
.lookup(&LANG, id)
|
||||||
|
.expect("Failed to get message with given id")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::consts::launcher_dir;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Dxvk {
|
|
||||||
pub builds: PathBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Dxvk {
|
|
||||||
fn default() -> Self {
|
|
||||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
builds: launcher_dir.join("dxvks")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Dxvk {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
builds: match value.get("builds") {
|
|
||||||
Some(value) => match value.as_str() {
|
|
||||||
Some(value) => PathBuf::from(value),
|
|
||||||
None => default.builds
|
|
||||||
},
|
|
||||||
None => default.builds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum Fps {
|
|
||||||
/// 90
|
|
||||||
Ninety,
|
|
||||||
|
|
||||||
/// 120
|
|
||||||
HundredTwenty,
|
|
||||||
|
|
||||||
/// 144
|
|
||||||
HundredFourtyFour,
|
|
||||||
|
|
||||||
/// 165
|
|
||||||
HundredSixtyFive,
|
|
||||||
|
|
||||||
/// 180
|
|
||||||
HundredEighty,
|
|
||||||
|
|
||||||
/// 200
|
|
||||||
TwoHundred,
|
|
||||||
|
|
||||||
/// 240
|
|
||||||
TwoHundredFourty,
|
|
||||||
|
|
||||||
Custom(u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Fps {
|
|
||||||
pub fn list() -> Vec<Self> {
|
|
||||||
vec![
|
|
||||||
Self::Ninety,
|
|
||||||
Self::HundredTwenty,
|
|
||||||
Self::HundredFourtyFour,
|
|
||||||
Self::HundredSixtyFive,
|
|
||||||
Self::HundredEighty,
|
|
||||||
Self::TwoHundred,
|
|
||||||
Self::TwoHundredFourty
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_model() -> gtk::StringList {
|
|
||||||
let model = gtk::StringList::new(&[]);
|
|
||||||
|
|
||||||
model.append("Custom");
|
|
||||||
|
|
||||||
for res in Self::list() {
|
|
||||||
model.append(&res.to_num().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
model
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_num(fps: u64) -> Self {
|
|
||||||
match fps {
|
|
||||||
90 => Self::Ninety,
|
|
||||||
120 => Self::HundredTwenty,
|
|
||||||
144 => Self::HundredFourtyFour,
|
|
||||||
165 => Self::HundredSixtyFive,
|
|
||||||
180 => Self::HundredEighty,
|
|
||||||
200 => Self::TwoHundred,
|
|
||||||
240 => Self::TwoHundredFourty,
|
|
||||||
num => Self::Custom(num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_num(&self) -> u64 {
|
|
||||||
match self {
|
|
||||||
Self::Ninety => 90,
|
|
||||||
Self::HundredTwenty => 120,
|
|
||||||
Self::HundredFourtyFour => 144,
|
|
||||||
Self::HundredSixtyFive => 165,
|
|
||||||
Self::HundredEighty => 180,
|
|
||||||
Self::TwoHundred => 200,
|
|
||||||
Self::TwoHundredFourty => 240,
|
|
||||||
Self::Custom(num) => *num
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
pub mod fps;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::fps::Fps;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
pub fps: u64,
|
|
||||||
pub power_saving: bool,
|
|
||||||
pub monitor: u64,
|
|
||||||
pub window_mode: u64,
|
|
||||||
pub priority: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
fps: 120,
|
|
||||||
power_saving: false,
|
|
||||||
monitor: 1,
|
|
||||||
window_mode: 0,
|
|
||||||
priority: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Config {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
fps: match value.get("fps") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.fps),
|
|
||||||
None => default.fps
|
|
||||||
},
|
|
||||||
|
|
||||||
power_saving: match value.get("power_saving") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.power_saving),
|
|
||||||
None => default.power_saving
|
|
||||||
},
|
|
||||||
|
|
||||||
monitor: match value.get("monitor") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.monitor),
|
|
||||||
None => default.monitor
|
|
||||||
},
|
|
||||||
|
|
||||||
window_mode: match value.get("window_mode") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.window_mode),
|
|
||||||
None => default.window_mode
|
|
||||||
},
|
|
||||||
|
|
||||||
priority: match value.get("priority") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.priority),
|
|
||||||
None => default.priority
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::consts::launcher_dir;
|
|
||||||
|
|
||||||
pub mod config;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::config::Config;
|
|
||||||
|
|
||||||
pub use super::config::prelude::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct FpsUnlocker {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub enabled: bool,
|
|
||||||
pub config: Config
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FpsUnlocker {
|
|
||||||
fn default() -> Self {
|
|
||||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: launcher_dir.join("fps-unlocker"),
|
|
||||||
enabled: false,
|
|
||||||
config: Config::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for FpsUnlocker {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: match value.get("path") {
|
|
||||||
Some(value) => match value.as_str() {
|
|
||||||
Some(value) => PathBuf::from(value),
|
|
||||||
None => default.path
|
|
||||||
},
|
|
||||||
None => default.path
|
|
||||||
},
|
|
||||||
|
|
||||||
enabled: match value.get("enabled") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.enabled),
|
|
||||||
None => default.enabled
|
|
||||||
},
|
|
||||||
|
|
||||||
config: match value.get("config") {
|
|
||||||
Some(value) => Config::from(value),
|
|
||||||
None => default.config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct Fsr {
|
|
||||||
pub strength: u64,
|
|
||||||
pub enabled: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Fsr {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
strength: 2,
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Fsr {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
strength: match value.get("strength") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.strength),
|
|
||||||
None => default.strength
|
|
||||||
},
|
|
||||||
|
|
||||||
enabled: match value.get("enabled") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.enabled),
|
|
||||||
None => default.enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Fsr {
|
|
||||||
/// Get environment variables corresponding to used amd fsr options
|
|
||||||
pub fn get_env_vars(&self) -> HashMap<&str, String> {
|
|
||||||
if self.enabled {
|
|
||||||
HashMap::from([
|
|
||||||
("WINE_FULLSCREEN_FSR", String::from("1")),
|
|
||||||
("WINE_FULLSCREEN_FSR_STRENGTH", self.strength.to_string())
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
HashMap::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
|
||||||
pub struct Framerate {
|
|
||||||
pub focused: u64,
|
|
||||||
pub unfocused: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Framerate {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
focused: match value.get("focused") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.focused),
|
|
||||||
None => default.focused
|
|
||||||
},
|
|
||||||
|
|
||||||
unfocused: match value.get("unfocused") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.unfocused),
|
|
||||||
None => default.unfocused
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
pub mod size;
|
|
||||||
pub mod framerate;
|
|
||||||
pub mod window_type;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::Gamescope;
|
|
||||||
pub use super::size::Size;
|
|
||||||
pub use super::framerate::Framerate;
|
|
||||||
pub use super::window_type::WindowType;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct Gamescope {
|
|
||||||
pub enabled: bool,
|
|
||||||
pub game: Size,
|
|
||||||
pub gamescope: Size,
|
|
||||||
pub framerate: Framerate,
|
|
||||||
pub integer_scaling: bool,
|
|
||||||
pub fsr: bool,
|
|
||||||
pub nis: bool,
|
|
||||||
pub window_type: WindowType
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Gamescope {
|
|
||||||
pub fn get_command(&self) -> Option<String> {
|
|
||||||
// https://github.com/bottlesdevs/Bottles/blob/b908311348ed1184ead23dd76f9d8af41ff24082/src/backend/wine/winecommand.py#L478
|
|
||||||
if self.enabled {
|
|
||||||
let mut gamescope = String::from("gamescope");
|
|
||||||
|
|
||||||
// Set window type
|
|
||||||
match self.window_type {
|
|
||||||
WindowType::Borderless => gamescope += " -b",
|
|
||||||
WindowType::Fullscreen => gamescope += " -f"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set game width
|
|
||||||
if self.game.width > 0 {
|
|
||||||
gamescope += &format!(" -w {}", self.game.width);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set game height
|
|
||||||
if self.game.height > 0 {
|
|
||||||
gamescope += &format!(" -h {}", self.game.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set gamescope width
|
|
||||||
if self.gamescope.width > 0 {
|
|
||||||
gamescope += &format!(" -W {}", self.gamescope.width);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set gamescope height
|
|
||||||
if self.gamescope.height > 0 {
|
|
||||||
gamescope += &format!(" -H {}", self.gamescope.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set focused framerate limit
|
|
||||||
if self.framerate.focused > 0 {
|
|
||||||
gamescope += &format!(" -r {}", self.framerate.focused);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set unfocused framerate limit
|
|
||||||
if self.framerate.unfocused > 0 {
|
|
||||||
gamescope += &format!(" -o {}", self.framerate.unfocused);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set integer scaling
|
|
||||||
if self.integer_scaling {
|
|
||||||
gamescope += " -n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set FSR support
|
|
||||||
if self.fsr {
|
|
||||||
gamescope += " -U";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set NIS (Nvidia Image Scaling) support
|
|
||||||
if self.nis {
|
|
||||||
gamescope += " -Y";
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(gamescope)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Gamescope {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enabled: false,
|
|
||||||
game: Size::default(),
|
|
||||||
gamescope: Size::default(),
|
|
||||||
framerate: Framerate::default(),
|
|
||||||
integer_scaling: true,
|
|
||||||
fsr: false,
|
|
||||||
nis: false,
|
|
||||||
window_type: WindowType::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Gamescope {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
enabled: match value.get("enabled") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.enabled),
|
|
||||||
None => default.enabled
|
|
||||||
},
|
|
||||||
|
|
||||||
game: match value.get("game") {
|
|
||||||
Some(value) => Size::from(value),
|
|
||||||
None => default.game
|
|
||||||
},
|
|
||||||
|
|
||||||
gamescope: match value.get("gamescope") {
|
|
||||||
Some(value) => Size::from(value),
|
|
||||||
None => default.gamescope
|
|
||||||
},
|
|
||||||
|
|
||||||
framerate: match value.get("framerate") {
|
|
||||||
Some(value) => Framerate::from(value),
|
|
||||||
None => default.framerate
|
|
||||||
},
|
|
||||||
|
|
||||||
integer_scaling: match value.get("integer_scaling") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.integer_scaling),
|
|
||||||
None => default.integer_scaling
|
|
||||||
},
|
|
||||||
|
|
||||||
fsr: match value.get("fsr") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.fsr),
|
|
||||||
None => default.fsr
|
|
||||||
},
|
|
||||||
|
|
||||||
nis: match value.get("nis") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.nis),
|
|
||||||
None => default.nis
|
|
||||||
},
|
|
||||||
|
|
||||||
window_type: match value.get("window_type") {
|
|
||||||
Some(value) => WindowType::from(value),
|
|
||||||
None => default.window_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
|
||||||
pub struct Size {
|
|
||||||
pub width: u64,
|
|
||||||
pub height: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Size {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
width: match value.get("width") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.width),
|
|
||||||
None => default.width
|
|
||||||
},
|
|
||||||
|
|
||||||
height: match value.get("height") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.height),
|
|
||||||
None => default.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum WindowType {
|
|
||||||
Borderless,
|
|
||||||
Fullscreen
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WindowType {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Borderless
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for WindowType {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
serde_json::from_value(value.clone()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::config::Config;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum HUD {
|
|
||||||
None,
|
|
||||||
DXVK,
|
|
||||||
MangoHUD
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for HUD {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for HUD {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
serde_json::from_value(value.clone()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u32> for HUD {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
0 => Ok(Self::None),
|
|
||||||
1 => Ok(Self::DXVK),
|
|
||||||
2 => Ok(Self::MangoHUD),
|
|
||||||
_ => Err(String::from("Failed to convert number to HUD enum"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
|
||||||
impl Into<u32> for HUD {
|
|
||||||
fn into(self) -> u32 {
|
|
||||||
match self {
|
|
||||||
Self::None => 0,
|
|
||||||
Self::DXVK => 1,
|
|
||||||
Self::MangoHUD => 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HUD {
|
|
||||||
/// Get environment variables corresponding to used wine hud
|
|
||||||
pub fn get_env_vars(&self, config: &Config) -> HashMap<&str, &str> {
|
|
||||||
match self {
|
|
||||||
Self::None => HashMap::new(),
|
|
||||||
Self::DXVK => HashMap::from([
|
|
||||||
("DXVK_HUD", "fps,frametimes,version,gpuload")
|
|
||||||
]),
|
|
||||||
Self::MangoHUD => {
|
|
||||||
// Don't show mangohud if gamescope is enabled
|
|
||||||
// otherwise it'll be doubled
|
|
||||||
if config.game.enhancements.gamescope.enabled {
|
|
||||||
HashMap::new()
|
|
||||||
} else {
|
|
||||||
HashMap::from([
|
|
||||||
("MANGOHUD", "1")
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
pub mod fsr;
|
|
||||||
pub mod hud;
|
|
||||||
pub mod fps_unlocker;
|
|
||||||
pub mod gamescope;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::gamescope::prelude::*;
|
|
||||||
pub use super::fps_unlocker::prelude::*;
|
|
||||||
|
|
||||||
pub use super::Enhancements;
|
|
||||||
pub use super::fsr::Fsr;
|
|
||||||
pub use super::hud::HUD;
|
|
||||||
pub use super::fps_unlocker::FpsUnlocker;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct Enhancements {
|
|
||||||
pub fsr: Fsr,
|
|
||||||
pub gamemode: bool,
|
|
||||||
pub hud: HUD,
|
|
||||||
pub fps_unlocker: FpsUnlocker,
|
|
||||||
pub gamescope: Gamescope
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Enhancements {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
fsr: match value.get("fsr") {
|
|
||||||
Some(value) => Fsr::from(value),
|
|
||||||
None => default.fsr
|
|
||||||
},
|
|
||||||
|
|
||||||
gamemode: match value.get("gamemode") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.gamemode),
|
|
||||||
None => default.gamemode
|
|
||||||
},
|
|
||||||
|
|
||||||
hud: match value.get("hud") {
|
|
||||||
Some(value) => HUD::from(value),
|
|
||||||
None => default.hud
|
|
||||||
},
|
|
||||||
|
|
||||||
fps_unlocker: match value.get("fps_unlocker") {
|
|
||||||
Some(value) => FpsUnlocker::from(value),
|
|
||||||
None => default.fps_unlocker
|
|
||||||
},
|
|
||||||
|
|
||||||
gamescope: match value.get("gamescope") {
|
|
||||||
Some(value) => Gamescope::from(value),
|
|
||||||
None => default.gamescope
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::consts::launcher_dir;
|
|
||||||
|
|
||||||
pub mod wine;
|
|
||||||
pub mod dxvk;
|
|
||||||
pub mod enhancements;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::enhancements::prelude::*;
|
|
||||||
pub use super::wine::prelude::*;
|
|
||||||
|
|
||||||
pub use super::Game;
|
|
||||||
pub use super::dxvk::Dxvk;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Game {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub voices: Vec<String>,
|
|
||||||
pub wine: prelude::Wine,
|
|
||||||
pub dxvk: prelude::Dxvk,
|
|
||||||
pub enhancements: prelude::Enhancements,
|
|
||||||
pub environment: HashMap<String, String>,
|
|
||||||
pub command: Option<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Game {
|
|
||||||
fn default() -> Self {
|
|
||||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: launcher_dir.join("game/drive_c/Program Files/Genshin Impact"),
|
|
||||||
voices: vec![
|
|
||||||
String::from("en-us")
|
|
||||||
],
|
|
||||||
wine: Wine::default(),
|
|
||||||
dxvk: Dxvk::default(),
|
|
||||||
enhancements: Enhancements::default(),
|
|
||||||
environment: HashMap::new(),
|
|
||||||
command: None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Game {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: match value.get("path") {
|
|
||||||
Some(value) => match value.as_str() {
|
|
||||||
Some(value) => PathBuf::from(value),
|
|
||||||
None => default.path
|
|
||||||
},
|
|
||||||
None => default.path
|
|
||||||
},
|
|
||||||
|
|
||||||
voices: match value.get("voices") {
|
|
||||||
Some(value) => match value.as_array() {
|
|
||||||
Some(values) => {
|
|
||||||
let mut voices = Vec::new();
|
|
||||||
|
|
||||||
for value in values {
|
|
||||||
if let Some(voice) = value.as_str() {
|
|
||||||
voices.push(voice.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
voices
|
|
||||||
},
|
|
||||||
None => default.voices
|
|
||||||
},
|
|
||||||
None => default.voices
|
|
||||||
},
|
|
||||||
|
|
||||||
wine: match value.get("wine") {
|
|
||||||
Some(value) => Wine::from(value),
|
|
||||||
None => default.wine
|
|
||||||
},
|
|
||||||
|
|
||||||
dxvk: match value.get("dxvk") {
|
|
||||||
Some(value) => Dxvk::from(value),
|
|
||||||
None => default.dxvk
|
|
||||||
},
|
|
||||||
|
|
||||||
enhancements: match value.get("enhancements") {
|
|
||||||
Some(value) => Enhancements::from(value),
|
|
||||||
None => default.enhancements
|
|
||||||
},
|
|
||||||
|
|
||||||
environment: match value.get("environment") {
|
|
||||||
Some(value) => match value.as_object() {
|
|
||||||
Some(values) => {
|
|
||||||
let mut vars = HashMap::new();
|
|
||||||
|
|
||||||
for (name, value) in values {
|
|
||||||
if let Some(value) = value.as_str() {
|
|
||||||
vars.insert(name.clone(), value.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vars
|
|
||||||
},
|
|
||||||
None => default.environment
|
|
||||||
},
|
|
||||||
None => default.environment
|
|
||||||
},
|
|
||||||
|
|
||||||
command: match value.get("command") {
|
|
||||||
Some(value) => {
|
|
||||||
if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
match value.as_str() {
|
|
||||||
Some(value) => Some(value.to_string()),
|
|
||||||
None => default.command
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => default.command
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::consts::launcher_dir;
|
|
||||||
|
|
||||||
pub mod wine_sync;
|
|
||||||
pub mod wine_lang;
|
|
||||||
pub mod virtual_desktop;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::Wine;
|
|
||||||
pub use super::wine_sync::WineSync;
|
|
||||||
pub use super::wine_lang::WineLang;
|
|
||||||
pub use super::virtual_desktop::VirtualDesktop;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Wine {
|
|
||||||
pub prefix: PathBuf,
|
|
||||||
pub builds: PathBuf,
|
|
||||||
pub selected: Option<String>,
|
|
||||||
pub sync: WineSync,
|
|
||||||
pub language: WineLang,
|
|
||||||
pub borderless: bool,
|
|
||||||
pub virtual_desktop: VirtualDesktop
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Wine {
|
|
||||||
fn default() -> Self {
|
|
||||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
prefix: launcher_dir.join("game"),
|
|
||||||
builds: launcher_dir.join("runners"),
|
|
||||||
selected: None,
|
|
||||||
sync: WineSync::default(),
|
|
||||||
language: WineLang::default(),
|
|
||||||
borderless: false,
|
|
||||||
virtual_desktop: VirtualDesktop::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Wine {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
prefix: match value.get("prefix") {
|
|
||||||
Some(value) => match value.as_str() {
|
|
||||||
Some(value) => PathBuf::from(value),
|
|
||||||
None => default.prefix
|
|
||||||
},
|
|
||||||
None => default.prefix
|
|
||||||
},
|
|
||||||
|
|
||||||
builds: match value.get("builds") {
|
|
||||||
Some(value) => match value.as_str() {
|
|
||||||
Some(value) => PathBuf::from(value),
|
|
||||||
None => default.builds
|
|
||||||
},
|
|
||||||
None => default.builds
|
|
||||||
},
|
|
||||||
|
|
||||||
selected: match value.get("selected") {
|
|
||||||
Some(value) => {
|
|
||||||
if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
match value.as_str() {
|
|
||||||
Some(value) => Some(value.to_string()),
|
|
||||||
None => default.selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => default.selected
|
|
||||||
},
|
|
||||||
|
|
||||||
sync: match value.get("sync") {
|
|
||||||
Some(value) => WineSync::from(value),
|
|
||||||
None => default.sync
|
|
||||||
},
|
|
||||||
|
|
||||||
language: match value.get("language") {
|
|
||||||
Some(value) => WineLang::from(value),
|
|
||||||
None => default.language
|
|
||||||
},
|
|
||||||
|
|
||||||
borderless: match value.get("borderless") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.borderless),
|
|
||||||
None => default.borderless
|
|
||||||
},
|
|
||||||
|
|
||||||
virtual_desktop: match value.get("virtual_desktop") {
|
|
||||||
Some(value) => VirtualDesktop::from(value),
|
|
||||||
None => default.virtual_desktop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::config::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct VirtualDesktop {
|
|
||||||
pub enabled: bool,
|
|
||||||
pub width: u64,
|
|
||||||
pub height: u64
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VirtualDesktop {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enabled: false,
|
|
||||||
width: 1920,
|
|
||||||
height: 1080
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for VirtualDesktop {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
enabled: match value.get("enabled") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.enabled),
|
|
||||||
None => default.enabled
|
|
||||||
},
|
|
||||||
|
|
||||||
width: match value.get("width") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.width),
|
|
||||||
None => default.width
|
|
||||||
},
|
|
||||||
|
|
||||||
height: match value.get("height") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.height),
|
|
||||||
None => default.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualDesktop {
|
|
||||||
pub fn get_resolution(&self) -> Resolution {
|
|
||||||
Resolution::from_pair(self.width, self.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_command(&self) -> Option<String> {
|
|
||||||
if self.enabled {
|
|
||||||
Some(format!("explorer /desktop=animegame,{}x{}", self.width, self.height))
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum WineLang {
|
|
||||||
System,
|
|
||||||
English,
|
|
||||||
Russian,
|
|
||||||
German,
|
|
||||||
Portuguese,
|
|
||||||
Polish,
|
|
||||||
French,
|
|
||||||
Spanish,
|
|
||||||
Chinese,
|
|
||||||
Japanese,
|
|
||||||
Korean
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WineLang {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::System
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for WineLang {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
serde_json::from_value(value.clone()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
|
||||||
impl Into<u32> for WineLang {
|
|
||||||
fn into(self) -> u32 {
|
|
||||||
for (i, lang) in Self::list().into_iter().enumerate() {
|
|
||||||
if lang == self {
|
|
||||||
return i as u32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WineLang {
|
|
||||||
pub fn list() -> Vec<Self> {
|
|
||||||
vec![
|
|
||||||
Self::System,
|
|
||||||
Self::English,
|
|
||||||
Self::Russian,
|
|
||||||
Self::German,
|
|
||||||
Self::Portuguese,
|
|
||||||
Self::Polish,
|
|
||||||
Self::French,
|
|
||||||
Self::Spanish,
|
|
||||||
Self::Chinese,
|
|
||||||
Self::Japanese,
|
|
||||||
Self::Korean
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_model() -> gtk::StringList {
|
|
||||||
let model = gtk::StringList::new(&[]);
|
|
||||||
|
|
||||||
for lang in Self::list() {
|
|
||||||
model.append(&lang.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
model
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get environment variables corresponding to used wine language
|
|
||||||
pub fn get_env_vars(&self) -> HashMap<&str, &str> {
|
|
||||||
HashMap::from([("LANG", match self {
|
|
||||||
Self::System => return HashMap::new(),
|
|
||||||
|
|
||||||
Self::English => "en_US.UTF8",
|
|
||||||
Self::Russian => "ru_RU.UTF8",
|
|
||||||
Self::German => "de_DE.UTF8",
|
|
||||||
Self::Portuguese => "pt_PT.UTF8",
|
|
||||||
Self::Polish => "pl_PL.UTF8",
|
|
||||||
Self::French => "fr_FR.UTF8",
|
|
||||||
Self::Spanish => "es_ES.UTF8",
|
|
||||||
Self::Chinese => "zh_CN.UTF8",
|
|
||||||
Self::Japanese => "ja_JP.UTF8",
|
|
||||||
Self::Korean => "ko_KR.UTF8"
|
|
||||||
})])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for WineLang {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&format!("{:?}", self))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum WineSync {
|
|
||||||
None,
|
|
||||||
ESync,
|
|
||||||
FSync,
|
|
||||||
Futex2
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WineSync {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::FSync
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for WineSync {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
serde_json::from_value(value.clone()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u32> for WineSync {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
0 => Ok(Self::None),
|
|
||||||
1 => Ok(Self::ESync),
|
|
||||||
2 => Ok(Self::FSync),
|
|
||||||
3 => Ok(Self::Futex2),
|
|
||||||
|
|
||||||
_ => Err(String::from("Failed to convert number to WineSync enum"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::from_over_into)]
|
|
||||||
impl Into<u32> for WineSync {
|
|
||||||
fn into(self) -> u32 {
|
|
||||||
match self {
|
|
||||||
Self::None => 0,
|
|
||||||
Self::ESync => 1,
|
|
||||||
Self::FSync => 2,
|
|
||||||
Self::Futex2 => 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WineSync {
|
|
||||||
/// Get environment variables corresponding to used wine sync
|
|
||||||
pub fn get_env_vars(&self) -> HashMap<&str, &str> {
|
|
||||||
HashMap::from([(match self {
|
|
||||||
Self::None => return HashMap::new(),
|
|
||||||
|
|
||||||
Self::ESync => "WINEESYNC",
|
|
||||||
Self::FSync => "WINEFSYNC",
|
|
||||||
Self::Futex2 => "WINEFSYNC_FUTEX2"
|
|
||||||
}, "1")])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use anime_game_core::genshin::consts::GameEdition as CoreGameEdition;
|
|
||||||
|
|
||||||
use crate::lib::consts::launcher_dir;
|
|
||||||
|
|
||||||
pub mod repairer;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::Launcher;
|
|
||||||
pub use super::repairer::Repairer;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum GameEdition {
|
|
||||||
Global,
|
|
||||||
China
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for GameEdition {
|
|
||||||
fn default() -> Self {
|
|
||||||
let locale = match std::env::var("LC_ALL") {
|
|
||||||
Ok(locale) => locale,
|
|
||||||
Err(_) => match std::env::var("LC_MESSAGES") {
|
|
||||||
Ok(locale) => locale,
|
|
||||||
Err(_) => match std::env::var("LANG") {
|
|
||||||
Ok(locale) => locale,
|
|
||||||
Err(_) => return Self::Global
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if locale.len() > 4 && &locale[..5].to_lowercase() == "zh_cn" {
|
|
||||||
Self::China
|
|
||||||
} else {
|
|
||||||
Self::Global
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<GameEdition> for CoreGameEdition {
|
|
||||||
fn from(edition: GameEdition) -> Self {
|
|
||||||
match edition {
|
|
||||||
GameEdition::Global => CoreGameEdition::Global,
|
|
||||||
GameEdition::China => CoreGameEdition::China
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<CoreGameEdition> for GameEdition {
|
|
||||||
fn from(edition: CoreGameEdition) -> Self {
|
|
||||||
match edition {
|
|
||||||
CoreGameEdition::Global => Self::Global,
|
|
||||||
CoreGameEdition::China => Self::China
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Launcher {
|
|
||||||
pub language: String,
|
|
||||||
pub temp: Option<PathBuf>,
|
|
||||||
pub speed_limit: u64,
|
|
||||||
pub repairer: Repairer,
|
|
||||||
pub edition: GameEdition
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Launcher {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
language: String::from("en-us"),
|
|
||||||
temp: launcher_dir(),
|
|
||||||
speed_limit: 0,
|
|
||||||
repairer: Repairer::default(),
|
|
||||||
edition: GameEdition::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Launcher {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
language: match value.get("language") {
|
|
||||||
Some(value) => value.as_str().unwrap_or(&default.language).to_string(),
|
|
||||||
None => default.language
|
|
||||||
},
|
|
||||||
|
|
||||||
temp: match value.get("temp") {
|
|
||||||
Some(value) => {
|
|
||||||
if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
match value.as_str() {
|
|
||||||
Some(value) => Some(PathBuf::from(value)),
|
|
||||||
None => default.temp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => default.temp
|
|
||||||
},
|
|
||||||
|
|
||||||
speed_limit: match value.get("speed_limit") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.speed_limit),
|
|
||||||
None => default.speed_limit
|
|
||||||
},
|
|
||||||
|
|
||||||
repairer: match value.get("repairer") {
|
|
||||||
Some(value) => Repairer::from(value),
|
|
||||||
None => default.repairer
|
|
||||||
},
|
|
||||||
|
|
||||||
edition: match value.get("edition") {
|
|
||||||
Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.edition),
|
|
||||||
None => default.edition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Repairer {
|
|
||||||
pub threads: u64,
|
|
||||||
pub fast: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Repairer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
threads: 4,
|
|
||||||
fast: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Repairer {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
threads: match value.get("threads") {
|
|
||||||
Some(value) => value.as_u64().unwrap_or(default.threads),
|
|
||||||
None => default.threads
|
|
||||||
},
|
|
||||||
|
|
||||||
fast: match value.get("fast") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.fast),
|
|
||||||
None => default.fast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use wincompatlib::dxvk::Dxvk;
|
|
||||||
|
|
||||||
use crate::lib;
|
|
||||||
use super::consts::*;
|
|
||||||
use super::wine::{
|
|
||||||
Version as WineVersion,
|
|
||||||
List as WineList
|
|
||||||
};
|
|
||||||
use super::dxvk::{
|
|
||||||
Version as DxvkVersion,
|
|
||||||
List as DxvkList
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod launcher;
|
|
||||||
pub mod game;
|
|
||||||
pub mod patch;
|
|
||||||
pub mod resolution;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::launcher::prelude::*;
|
|
||||||
pub use super::game::prelude::*;
|
|
||||||
|
|
||||||
pub use super::patch::Patch;
|
|
||||||
pub use super::resolution::Resolution;
|
|
||||||
}
|
|
||||||
|
|
||||||
use prelude::*;
|
|
||||||
|
|
||||||
static mut CONFIG: Option<Config> = None;
|
|
||||||
|
|
||||||
/// Get config data
|
|
||||||
///
|
|
||||||
/// This method will load config from file once and store it into the memory.
|
|
||||||
/// If you know that the config file was updated - you should run `get_raw` method
|
|
||||||
/// that always loads config directly from the file. This will also update in-memory config
|
|
||||||
pub fn get() -> anyhow::Result<Config> {
|
|
||||||
unsafe {
|
|
||||||
match &CONFIG {
|
|
||||||
Some(config) => Ok(config.clone()),
|
|
||||||
None => get_raw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get config data
|
|
||||||
///
|
|
||||||
/// This method will always load data directly from the file and update in-memory config
|
|
||||||
pub fn get_raw() -> anyhow::Result<Config> {
|
|
||||||
match config_file() {
|
|
||||||
Some(path) => {
|
|
||||||
// Try to read config if the file exists
|
|
||||||
if Path::new(&path).exists() {
|
|
||||||
let mut file = File::open(path)?;
|
|
||||||
let mut json = String::new();
|
|
||||||
|
|
||||||
file.read_to_string(&mut json)?;
|
|
||||||
|
|
||||||
match serde_json::from_str(&json) {
|
|
||||||
Ok(config) => {
|
|
||||||
let config = Config::from(&config);
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
CONFIG = Some(config.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
},
|
|
||||||
Err(err) => Err(anyhow::anyhow!("Failed to decode data from json format: {}", err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise create default config file
|
|
||||||
else {
|
|
||||||
update_raw(Config::default())?;
|
|
||||||
|
|
||||||
Ok(Config::default())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => Err(anyhow::anyhow!("Failed to get config file path"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update in-memory config data
|
|
||||||
///
|
|
||||||
/// Use `update_raw` if you want to update config file itself
|
|
||||||
pub fn update(config: Config) {
|
|
||||||
unsafe {
|
|
||||||
CONFIG = Some(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update config file
|
|
||||||
///
|
|
||||||
/// This method will also update in-memory config data
|
|
||||||
pub fn update_raw(config: Config) -> anyhow::Result<()> {
|
|
||||||
update(config.clone());
|
|
||||||
|
|
||||||
match config_file() {
|
|
||||||
Some(path) => {
|
|
||||||
let mut file = File::create(&path)?;
|
|
||||||
|
|
||||||
match serde_json::to_string_pretty(&config) {
|
|
||||||
Ok(json) => {
|
|
||||||
file.write_all(json.as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
Err(err) => Err(anyhow::anyhow!("Failed to encode data into json format: {}", err.to_string()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => Err(anyhow::anyhow!("Failed to get config file path"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update config file from the in-memory saved config
|
|
||||||
pub fn flush() -> anyhow::Result<()> {
|
|
||||||
unsafe {
|
|
||||||
match &CONFIG {
|
|
||||||
Some(config) => update_raw(config.clone()),
|
|
||||||
None => Err(anyhow::anyhow!("Config wasn't loaded into the memory"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct Config {
|
|
||||||
pub launcher: Launcher,
|
|
||||||
pub game: Game,
|
|
||||||
pub patch: Patch
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn try_get_selected_wine_info(&self) -> Option<WineVersion> {
|
|
||||||
match &self.game.wine.selected {
|
|
||||||
Some(selected) => {
|
|
||||||
WineList::get().iter()
|
|
||||||
.flat_map(|group| group.versions.clone())
|
|
||||||
.find(|version| version.name.eq(selected))
|
|
||||||
},
|
|
||||||
None => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to get a path to the wine64 executable based on `game.wine.builds` and `game.wine.selected`
|
|
||||||
///
|
|
||||||
/// Returns `Some("wine64")` if:
|
|
||||||
/// 1) `game.wine.selected = None`
|
|
||||||
/// 2) wine64 installed and available in system
|
|
||||||
pub fn try_get_wine_executable(&self) -> Option<PathBuf> {
|
|
||||||
match self.try_get_selected_wine_info() {
|
|
||||||
Some(selected) => Some(self.game.wine.builds.join(selected.name).join(selected.files.wine64)),
|
|
||||||
None => {
|
|
||||||
if lib::is_available("wine64") {
|
|
||||||
Some(PathBuf::from("wine64"))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to get DXVK version applied to wine prefix
|
|
||||||
///
|
|
||||||
/// Returns:
|
|
||||||
/// 1) `Ok(Some(..))` if version was found
|
|
||||||
/// 2) `Ok(None)` if version wasn't found, so too old or dxvk is not applied
|
|
||||||
/// 3) `Err(..)` if failed to get applied dxvk version, likely because wrong prefix path specified
|
|
||||||
pub fn try_get_selected_dxvk_info(&self) -> std::io::Result<Option<DxvkVersion>> {
|
|
||||||
Ok(match Dxvk::get_version(&self.game.wine.prefix)? {
|
|
||||||
Some(version) => {
|
|
||||||
DxvkList::get()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|group| group.versions.clone())
|
|
||||||
.find(move |dxvk| dxvk.version == version)
|
|
||||||
},
|
|
||||||
None => None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Config {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
launcher: match value.get("launcher") {
|
|
||||||
Some(value) => Launcher::from(value),
|
|
||||||
None => default.launcher
|
|
||||||
},
|
|
||||||
|
|
||||||
game: match value.get("game") {
|
|
||||||
Some(value) => Game::from(value),
|
|
||||||
None => default.game
|
|
||||||
},
|
|
||||||
|
|
||||||
patch: match value.get("patch") {
|
|
||||||
Some(value) => Patch::from(value),
|
|
||||||
None => default.patch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
|
|
||||||
use crate::lib::consts::launcher_dir;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Patch {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub servers: Vec<String>,
|
|
||||||
pub root: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Patch {
|
|
||||||
fn default() -> Self {
|
|
||||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: launcher_dir.join("patch"),
|
|
||||||
servers: vec![
|
|
||||||
"https://notabug.org/Krock/dawn".to_string(),
|
|
||||||
"https://codespace.gay/Maroxy/dawnin".to_string()
|
|
||||||
],
|
|
||||||
|
|
||||||
// Disable root requirement for patching if we're running launcher in flatpak
|
|
||||||
root: !Path::new("/.flatpak-info").exists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsonValue> for Patch {
|
|
||||||
fn from(value: &JsonValue) -> Self {
|
|
||||||
let default = Self::default();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
path: match value.get("path") {
|
|
||||||
Some(value) => match value.as_str() {
|
|
||||||
Some(value) => PathBuf::from(value),
|
|
||||||
None => default.path
|
|
||||||
},
|
|
||||||
None => default.path
|
|
||||||
},
|
|
||||||
|
|
||||||
servers: match value.get("servers") {
|
|
||||||
Some(value) => match value.as_array() {
|
|
||||||
Some(values) => {
|
|
||||||
let mut servers = Vec::new();
|
|
||||||
|
|
||||||
for value in values {
|
|
||||||
if let Some(server) = value.as_str() {
|
|
||||||
servers.push(server.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
servers
|
|
||||||
},
|
|
||||||
None => default.servers
|
|
||||||
},
|
|
||||||
None => default.servers
|
|
||||||
},
|
|
||||||
|
|
||||||
root: match value.get("root") {
|
|
||||||
Some(value) => value.as_bool().unwrap_or(default.root),
|
|
||||||
None => default.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum Resolution {
|
|
||||||
// qHD; 960x540
|
|
||||||
MiniHD,
|
|
||||||
|
|
||||||
// 1280x720
|
|
||||||
HD,
|
|
||||||
|
|
||||||
// 1920x1080
|
|
||||||
FullHD,
|
|
||||||
|
|
||||||
// 2560x1440
|
|
||||||
QuadHD,
|
|
||||||
|
|
||||||
// 3840x2160
|
|
||||||
UltraHD,
|
|
||||||
|
|
||||||
Custom(u64, u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resolution {
|
|
||||||
pub fn list() -> Vec<Self> {
|
|
||||||
vec![
|
|
||||||
Self::MiniHD,
|
|
||||||
Self::HD,
|
|
||||||
Self::FullHD,
|
|
||||||
Self::QuadHD,
|
|
||||||
Self::UltraHD
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_model() -> gtk::StringList {
|
|
||||||
let model = gtk::StringList::new(&[]);
|
|
||||||
|
|
||||||
model.append("Custom");
|
|
||||||
|
|
||||||
for res in Self::list() {
|
|
||||||
model.append(&res.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
model
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_pair(width: u64, height: u64) -> Self {
|
|
||||||
for res in Self::list() {
|
|
||||||
let pair = res.get_pair();
|
|
||||||
|
|
||||||
if pair.0 == width && pair.1 == height {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::Custom(width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_pair(&self) -> (u64, u64) {
|
|
||||||
match self {
|
|
||||||
Self::MiniHD => (960, 540),
|
|
||||||
Self::HD => (1280, 720),
|
|
||||||
Self::FullHD => (1920, 1080),
|
|
||||||
Self::QuadHD => (2560, 1440),
|
|
||||||
Self::UltraHD => (3840, 2160),
|
|
||||||
|
|
||||||
Self::Custom(w, h) => (*w, *h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Resolution {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let (w, h) = self.get_pair();
|
|
||||||
|
|
||||||
f.write_str(&format!("{w}x{h}"))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use cached::proc_macro::cached;
|
|
||||||
|
|
||||||
/// Timeout used by `anime_game_core::telemetry::is_disabled` to check acessibility of telemetry servers
|
|
||||||
pub const TELEMETRY_CHECK_TIMEOUT: Option<Duration> = Some(Duration::from_secs(3));
|
|
||||||
|
|
||||||
/// Timeout used by `anime_game_core::linux_patch::Patch::try_fetch` to fetch patch info
|
|
||||||
pub const PATCH_FETCHING_TIMEOUT: Option<Duration> = Some(Duration::from_secs(5));
|
|
||||||
|
|
||||||
#[cached]
|
|
||||||
pub fn launcher_dir() -> Option<PathBuf> {
|
|
||||||
dirs::data_dir().map(|dir| dir.join("anime-game-launcher"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cached]
|
|
||||||
pub fn config_file() -> Option<PathBuf> {
|
|
||||||
launcher_dir().map(|dir| dir.join("config.json"))
|
|
||||||
}
|
|
114
src/lib/dxvk.rs
114
src/lib/dxvk.rs
|
@ -1,114 +0,0 @@
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use std::process::Output;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
use wincompatlib::prelude::*;
|
|
||||||
|
|
||||||
use crate::lib::config;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref GROUPS: Vec<Group> = vec![
|
|
||||||
Group {
|
|
||||||
title: String::from("Vanilla"),
|
|
||||||
subtitle: None,
|
|
||||||
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/dxvk/vanilla.json")).unwrap().into_iter().take(12).collect()
|
|
||||||
},
|
|
||||||
Group {
|
|
||||||
title: String::from("Async"),
|
|
||||||
subtitle: Some(String::from("This version is not recommended for usage as can lead to anti-cheat detection. Automatically uses DXVK_ASYNC=1")),
|
|
||||||
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/dxvk/async.json")).unwrap().into_iter().take(12).collect()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct List;
|
|
||||||
|
|
||||||
impl List {
|
|
||||||
pub fn get() -> Vec<Group> {
|
|
||||||
GROUPS.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List only downloaded DXVK versions in some specific folder
|
|
||||||
pub fn list_downloaded<T: Into<PathBuf>>(folder: T) -> std::io::Result<Vec<Version>> {
|
|
||||||
let mut downloaded = Vec::new();
|
|
||||||
|
|
||||||
let list = Self::get();
|
|
||||||
|
|
||||||
for entry in std::fs::read_dir(folder.into())? {
|
|
||||||
let name = entry?.file_name();
|
|
||||||
|
|
||||||
for group in &list {
|
|
||||||
for version in &group.versions {
|
|
||||||
if name == version.name.as_str() {
|
|
||||||
downloaded.push(version.clone());
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(downloaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Group {
|
|
||||||
pub title: String,
|
|
||||||
pub subtitle: Option<String>,
|
|
||||||
pub versions: Vec<Version>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Version {
|
|
||||||
pub name: String,
|
|
||||||
pub version: String,
|
|
||||||
pub uri: String,
|
|
||||||
pub recommended: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Version {
|
|
||||||
pub fn latest() -> Result<Self, serde_json::Error> {
|
|
||||||
Ok(List::get()[0].versions[0].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_downloaded_in<T: Into<PathBuf>>(&self, folder: T) -> bool {
|
|
||||||
folder.into().join(&self.name).exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply<T: Into<PathBuf>>(&self, dxvks_folder: T, prefix_path: T) -> anyhow::Result<Output> {
|
|
||||||
let apply_path = dxvks_folder.into().join(&self.name).join("setup_dxvk.sh");
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
let (wine_path, wineserver_path, wineboot_path) = match config.try_get_selected_wine_info() {
|
|
||||||
Some(wine) => {
|
|
||||||
let wine_folder = config.game.wine.builds.join(wine.name);
|
|
||||||
|
|
||||||
let wine_path = wine_folder.join(wine.files.wine64);
|
|
||||||
let wineserver_path = wine_folder.join(wine.files.wineserver);
|
|
||||||
let wineboot_path = wine_folder.join(wine.files.wineboot);
|
|
||||||
|
|
||||||
(wine_path, wineserver_path, wineboot_path)
|
|
||||||
},
|
|
||||||
None => (PathBuf::from("wine64"), PathBuf::from("wineserver"), PathBuf::from("wineboot"))
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = Dxvk::install(
|
|
||||||
apply_path,
|
|
||||||
prefix_path.into(),
|
|
||||||
wine_path.clone(),
|
|
||||||
wine_path,
|
|
||||||
wineboot_path,
|
|
||||||
wineserver_path
|
|
||||||
);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(output) => Ok(output),
|
|
||||||
Err(err) => Err(err.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use super::FpsUnlockerConfig;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub struct ConfigSchema {
|
|
||||||
pub DllList: Vec<String>,
|
|
||||||
pub Priority: u64,
|
|
||||||
pub MonitorNum: u64,
|
|
||||||
pub CustomResY: u64,
|
|
||||||
pub CustomResX: u64,
|
|
||||||
pub FPSTarget: u64,
|
|
||||||
pub UsePowerSave: bool,
|
|
||||||
pub StartMinimized: bool,
|
|
||||||
pub IsExclusiveFullscreen: bool,
|
|
||||||
pub UseCustomRes: bool,
|
|
||||||
pub Fullscreen: bool,
|
|
||||||
pub PopupWindow: bool,
|
|
||||||
pub AutoClose: bool,
|
|
||||||
pub AutoDisableVSync: bool,
|
|
||||||
pub AutoStart: bool,
|
|
||||||
pub GamePath: Option<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ConfigSchema {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
DllList: vec![],
|
|
||||||
Priority: 3,
|
|
||||||
MonitorNum: 1,
|
|
||||||
CustomResY: 1080,
|
|
||||||
CustomResX: 1920,
|
|
||||||
FPSTarget: 120,
|
|
||||||
UsePowerSave: false,
|
|
||||||
IsExclusiveFullscreen: false,
|
|
||||||
UseCustomRes: false,
|
|
||||||
Fullscreen: false,
|
|
||||||
PopupWindow: false,
|
|
||||||
AutoDisableVSync: true,
|
|
||||||
GamePath: None,
|
|
||||||
|
|
||||||
// Launcher-specific settings
|
|
||||||
AutoStart: true,
|
|
||||||
AutoClose: true,
|
|
||||||
StartMinimized: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigSchema {
|
|
||||||
pub fn from_config(config: FpsUnlockerConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
FPSTarget: config.fps,
|
|
||||||
UsePowerSave: config.power_saving,
|
|
||||||
PopupWindow: config.window_mode == 1,
|
|
||||||
Fullscreen: config.window_mode == 2,
|
|
||||||
MonitorNum: config.monitor,
|
|
||||||
Priority: config.priority,
|
|
||||||
|
|
||||||
..Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn json(&self) -> serde_json::Result<String> {
|
|
||||||
serde_json::to_string(self)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use anime_game_core::installer::downloader::Downloader;
|
|
||||||
|
|
||||||
use crate::lib::config::game::enhancements::fps_unlocker::config::Config as FpsUnlockerConfig;
|
|
||||||
|
|
||||||
pub mod config_schema;
|
|
||||||
|
|
||||||
const LATEST_INFO: (&str, &str) = (
|
|
||||||
"6040a6f0be5dbf4d55d6b129cad47b5b",
|
|
||||||
"https://github.com/34736384/genshin-fps-unlock/releases/download/v2.0.0/unlockfps_clr.exe"
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct FpsUnlocker {
|
|
||||||
dir: PathBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FpsUnlocker {
|
|
||||||
/// Get FpsUnlocker from its containment directory
|
|
||||||
///
|
|
||||||
/// Returns
|
|
||||||
/// - `Err(..)` if failed to read `unlocker.exe` file
|
|
||||||
/// - `Ok(None)` if version is not latest
|
|
||||||
/// - `Ok(..)` if version is latest
|
|
||||||
pub fn from_dir<T: Into<PathBuf>>(dir: T) -> anyhow::Result<Option<Self>> {
|
|
||||||
let dir = dir.into();
|
|
||||||
|
|
||||||
let hash = format!("{:x}", md5::compute(std::fs::read(dir.join("unlocker.exe"))?));
|
|
||||||
|
|
||||||
Ok(if hash == LATEST_INFO.0 {
|
|
||||||
Some(Self { dir })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download FPS unlocker to specified directory
|
|
||||||
pub fn download<T: Into<PathBuf>>(dir: T) -> anyhow::Result<Self> {
|
|
||||||
let mut downloader = Downloader::new(LATEST_INFO.1)?;
|
|
||||||
|
|
||||||
let dir = dir.into();
|
|
||||||
|
|
||||||
// Create FPS unlocker folder if needed
|
|
||||||
if !dir.exists() {
|
|
||||||
std::fs::create_dir_all(&dir)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match downloader.download_to(dir.join("unlocker.exe"), |_, _| {}) {
|
|
||||||
Ok(_) => Ok(Self {
|
|
||||||
dir
|
|
||||||
}),
|
|
||||||
Err(err) => {
|
|
||||||
let err: std::io::Error = err.into();
|
|
||||||
|
|
||||||
Err(err.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_binary(&self) -> PathBuf {
|
|
||||||
Self::get_binary_in(&self.dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_binary_in<T: Into<PathBuf>>(dir: T) -> PathBuf {
|
|
||||||
dir.into().join("unlocker.exe")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dir(&self) -> &PathBuf {
|
|
||||||
&self.dir
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate and save FPS unlocker config file to the game's directory
|
|
||||||
pub fn update_config(&self, config: FpsUnlockerConfig) -> anyhow::Result<()> {
|
|
||||||
let config = config_schema::ConfigSchema::from_config(config);
|
|
||||||
|
|
||||||
Ok(std::fs::write(
|
|
||||||
self.dir.join("fps_config.json"),
|
|
||||||
config.json()?
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
}
|
|
187
src/lib/game.rs
187
src/lib/game.rs
|
@ -1,187 +0,0 @@
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use anime_game_core::genshin::telemetry;
|
|
||||||
|
|
||||||
use super::consts;
|
|
||||||
use super::config;
|
|
||||||
use super::fps_unlocker::FpsUnlocker;
|
|
||||||
|
|
||||||
/*#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Terminal {
|
|
||||||
GnomeTerminal,
|
|
||||||
Konsole,
|
|
||||||
Xfce4Terminal
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Terminal {
|
|
||||||
pub fn get_command(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Terminal::GnomeTerminal => "gnome-terminal",
|
|
||||||
Terminal::Konsole => "konsole",
|
|
||||||
Terminal::Xfce4Terminal => "xfce4-terminal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter() -> impl Iterator<Item = Terminal> {
|
|
||||||
[
|
|
||||||
Terminal::GnomeTerminal,
|
|
||||||
Terminal::Konsole,
|
|
||||||
Terminal::Xfce4Terminal
|
|
||||||
].into_iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_args(&self, bash_command: &str) -> Vec<String> {
|
|
||||||
match self {
|
|
||||||
Terminal::GnomeTerminal => vec![
|
|
||||||
String::from("--"),
|
|
||||||
String::from("bash"),
|
|
||||||
String::from("-c"),
|
|
||||||
format!("{} && bash", bash_command)
|
|
||||||
],
|
|
||||||
Terminal::Konsole | Terminal::Xfce4Terminal => vec![
|
|
||||||
String::from("--hold"),
|
|
||||||
String::from("-e"),
|
|
||||||
format!("\"bash -c '{} && bash'\"", bash_command)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to get GUI terminal installed in system
|
|
||||||
pub fn try_get_terminal() -> Option<Terminal> {
|
|
||||||
for terminal in Terminal::iter() {
|
|
||||||
if let Ok(output) = Command::new(terminal.get_command()).output() {
|
|
||||||
if output.status.success() {
|
|
||||||
return Some(terminal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/// Try to run the game
|
|
||||||
///
|
|
||||||
/// If `debug = true`, then the game will be run in the new terminal window
|
|
||||||
pub fn run() -> anyhow::Result<()> {
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
if !config.game.path.exists() {
|
|
||||||
return Err(anyhow::anyhow!("Game is not installed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let wine_executable = match config.try_get_wine_executable() {
|
|
||||||
Some(path) => path,
|
|
||||||
None => return Err(anyhow::anyhow!("Couldn't find wine executable"))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check telemetry servers
|
|
||||||
|
|
||||||
if let Some(server) = telemetry::is_disabled(consts::TELEMETRY_CHECK_TIMEOUT) {
|
|
||||||
return Err(anyhow::anyhow!("Telemetry server is not disabled: {server}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare fps unlocker
|
|
||||||
// 1) Download if needed
|
|
||||||
// 2) Generate config file
|
|
||||||
// 3) Generate fpsunlocker.bat from launcher.bat
|
|
||||||
|
|
||||||
if config.game.enhancements.fps_unlocker.enabled {
|
|
||||||
let unlocker = match FpsUnlocker::from_dir(&config.game.enhancements.fps_unlocker.path) {
|
|
||||||
Ok(Some(unlocker)) => unlocker,
|
|
||||||
|
|
||||||
other => {
|
|
||||||
// Ok(None) means unknown version, so we should delete it before downloading newer one
|
|
||||||
// because otherwise downloader will try to continue downloading "partially downloaded" file
|
|
||||||
if let Ok(None) = other {
|
|
||||||
std::fs::remove_file(FpsUnlocker::get_binary_in(&config.game.enhancements.fps_unlocker.path))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match FpsUnlocker::download(&config.game.enhancements.fps_unlocker.path) {
|
|
||||||
Ok(unlocker) => unlocker,
|
|
||||||
Err(err) => return Err(anyhow::anyhow!("Failed to download FPS unlocker: {err}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate FPS unlocker config file
|
|
||||||
if let Err(err) = unlocker.update_config(config.game.enhancements.fps_unlocker.config.clone()) {
|
|
||||||
return Err(anyhow::anyhow!("Failed to update FPS unlocker config: {err}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let bat_path = config.game.path.join("fpsunlocker.bat");
|
|
||||||
let original_bat_path = config.game.path.join("launcher.bat");
|
|
||||||
|
|
||||||
// Generate fpsunlocker.bat from launcher.bat
|
|
||||||
std::fs::write(bat_path, std::fs::read_to_string(original_bat_path)?
|
|
||||||
.replace("start GenshinImpact.exe %*", &format!("start GenshinImpact.exe %*\n\nZ:\ncd \"{}\"\nstart unlocker.exe", unlocker.dir().to_string_lossy()))
|
|
||||||
.replace("start YuanShen.exe %*", &format!("start YuanShen.exe %*\n\nZ:\ncd \"{}\"\nstart unlocker.exe", unlocker.dir().to_string_lossy())))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare bash -c '<command>'
|
|
||||||
|
|
||||||
let mut bash_chain = String::new();
|
|
||||||
|
|
||||||
if config.game.enhancements.gamemode {
|
|
||||||
bash_chain += "gamemoderun ";
|
|
||||||
}
|
|
||||||
|
|
||||||
bash_chain += &format!("'{}' ", wine_executable.to_string_lossy());
|
|
||||||
|
|
||||||
if let Some(virtual_desktop) = config.game.wine.virtual_desktop.get_command() {
|
|
||||||
bash_chain += &format!("{virtual_desktop} ");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash_chain += if config.game.enhancements.fps_unlocker.enabled { "fpsunlocker.bat " } else { "launcher.bat " };
|
|
||||||
|
|
||||||
if config.game.wine.borderless {
|
|
||||||
bash_chain += "-screen-fullscreen 0 -popupwindow ";
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://notabug.org/Krock/dawn/src/master/TWEAKS.md
|
|
||||||
if config.game.enhancements.fsr.enabled {
|
|
||||||
bash_chain += "-window-mode exclusive ";
|
|
||||||
}
|
|
||||||
|
|
||||||
// gamescope <params> -- <command to run>
|
|
||||||
if let Some(gamescope) = config.game.enhancements.gamescope.get_command() {
|
|
||||||
bash_chain = format!("{gamescope} -- {bash_chain}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let bash_chain = match &config.game.command {
|
|
||||||
Some(command) => command.replace("%command%", &bash_chain),
|
|
||||||
None => bash_chain
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut command = Command::new("bash");
|
|
||||||
|
|
||||||
command.arg("-c");
|
|
||||||
command.arg(&bash_chain);
|
|
||||||
|
|
||||||
// Setup environment
|
|
||||||
|
|
||||||
command.env("WINEARCH", "win64");
|
|
||||||
command.env("WINEPREFIX", &config.game.wine.prefix);
|
|
||||||
|
|
||||||
// Add DXVK_ASYNC=1 for dxvk-async builds automatically
|
|
||||||
if let Ok(Some(dxvk)) = &config.try_get_selected_dxvk_info() {
|
|
||||||
if dxvk.version.contains("async") {
|
|
||||||
command.env("DXVK_ASYNC", "1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
command.envs(config.game.wine.sync.get_env_vars());
|
|
||||||
command.envs(config.game.enhancements.hud.get_env_vars(&config));
|
|
||||||
command.envs(config.game.enhancements.fsr.get_env_vars());
|
|
||||||
command.envs(config.game.wine.language.get_env_vars());
|
|
||||||
|
|
||||||
command.envs(config.game.environment);
|
|
||||||
|
|
||||||
// Run command
|
|
||||||
|
|
||||||
println!("Running command: bash -c \"{}\"", bash_chain);
|
|
||||||
|
|
||||||
command.current_dir(config.game.path).spawn()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod states;
|
|
|
@ -1,128 +0,0 @@
|
||||||
use anime_game_core::prelude::*;
|
|
||||||
use anime_game_core::genshin::prelude::*;
|
|
||||||
|
|
||||||
use crate::lib::consts;
|
|
||||||
use crate::lib::config;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum LauncherState {
|
|
||||||
Launch,
|
|
||||||
|
|
||||||
/// Always contains `VersionDiff::Predownload`
|
|
||||||
PredownloadAvailable {
|
|
||||||
game: VersionDiff,
|
|
||||||
voices: Vec<VersionDiff>
|
|
||||||
},
|
|
||||||
|
|
||||||
PatchAvailable(Patch),
|
|
||||||
|
|
||||||
WineNotInstalled,
|
|
||||||
PrefixNotExists,
|
|
||||||
|
|
||||||
// Always contains `VersionDiff::Diff`
|
|
||||||
VoiceUpdateAvailable(VersionDiff),
|
|
||||||
|
|
||||||
/// Always contains `VersionDiff::Outdated`
|
|
||||||
VoiceOutdated(VersionDiff),
|
|
||||||
|
|
||||||
/// Always contains `VersionDiff::NotInstalled`
|
|
||||||
VoiceNotInstalled(VersionDiff),
|
|
||||||
|
|
||||||
// Always contains `VersionDiff::Diff`
|
|
||||||
GameUpdateAvailable(VersionDiff),
|
|
||||||
|
|
||||||
/// Always contains `VersionDiff::Outdated`
|
|
||||||
GameOutdated(VersionDiff),
|
|
||||||
|
|
||||||
/// Always contains `VersionDiff::NotInstalled`
|
|
||||||
GameNotInstalled(VersionDiff)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LauncherState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LauncherState {
|
|
||||||
pub fn get<T: Fn(&str)>(status: T) -> anyhow::Result<Self> {
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
// Check wine existence
|
|
||||||
if config.try_get_wine_executable().is_none() {
|
|
||||||
return Ok(Self::WineNotInstalled);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check prefix existence
|
|
||||||
if !config.game.wine.prefix.join("drive_c").exists() {
|
|
||||||
return Ok(Self::PrefixNotExists);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check game installation status
|
|
||||||
status("Updating game info...");
|
|
||||||
|
|
||||||
let game = Game::new(&config.game.path);
|
|
||||||
let diff = game.try_get_diff()?;
|
|
||||||
|
|
||||||
Ok(match diff {
|
|
||||||
VersionDiff::Latest(_) | VersionDiff::Predownload { .. } => {
|
|
||||||
status("Updating voice info...");
|
|
||||||
|
|
||||||
let mut predownload_voice = Vec::new();
|
|
||||||
|
|
||||||
for voice_package in &config.game.voices {
|
|
||||||
let mut voice_package = VoicePackage::with_locale(match VoiceLocale::from_str(voice_package) {
|
|
||||||
Some(locale) => locale,
|
|
||||||
None => return Err(anyhow::anyhow!("Incorrect voice locale \"{}\" specified in the config", voice_package))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
status(format!("Updating voice info ({})...", voice_package.locale().to_name()).as_str());
|
|
||||||
|
|
||||||
// Replace voice package struct with the one constructed in the game's folder
|
|
||||||
// so it'll properly calculate its difference instead of saying "not installed"
|
|
||||||
if voice_package.is_installed_in(&config.game.path) {
|
|
||||||
voice_package = match VoicePackage::new(get_voice_package_path(&config.game.path, voice_package.locale())) {
|
|
||||||
Some(locale) => locale,
|
|
||||||
None => return Err(anyhow::anyhow!("Failed to load {} voice package", voice_package.locale().to_name()))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let diff = voice_package.try_get_diff()?;
|
|
||||||
|
|
||||||
match diff {
|
|
||||||
VersionDiff::Latest(_) => (),
|
|
||||||
VersionDiff::Predownload { .. } => predownload_voice.push(diff),
|
|
||||||
|
|
||||||
VersionDiff::Diff { .. } => return Ok(Self::VoiceUpdateAvailable(diff)),
|
|
||||||
VersionDiff::Outdated { .. } => return Ok(Self::VoiceOutdated(diff)),
|
|
||||||
VersionDiff::NotInstalled { .. } => return Ok(Self::VoiceNotInstalled(diff))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status("Updating patch info...");
|
|
||||||
|
|
||||||
let patch = Patch::try_fetch(config.patch.servers.clone(), consts::PATCH_FETCHING_TIMEOUT)?;
|
|
||||||
|
|
||||||
if patch.is_applied(&config.game.path)? {
|
|
||||||
if let VersionDiff::Predownload { .. } = diff {
|
|
||||||
Self::PredownloadAvailable {
|
|
||||||
game: diff,
|
|
||||||
voices: predownload_voice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
Self::Launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
Self::PatchAvailable(patch)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
VersionDiff::Diff { .. } => Self::GameUpdateAvailable(diff),
|
|
||||||
VersionDiff::Outdated { .. } => Self::GameOutdated(diff),
|
|
||||||
VersionDiff::NotInstalled { .. } => Self::GameNotInstalled(diff)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
pub mod consts;
|
|
||||||
pub mod config;
|
|
||||||
pub mod game;
|
|
||||||
pub mod dxvk;
|
|
||||||
pub mod wine;
|
|
||||||
pub mod launcher;
|
|
||||||
pub mod prettify_bytes;
|
|
||||||
pub mod fps_unlocker;
|
|
||||||
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
/// Check if specified binary is available
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use crate::lib;
|
|
||||||
///
|
|
||||||
/// assert!(lib::is_available("bash"));
|
|
||||||
/// ```
|
|
||||||
#[allow(unused_must_use)]
|
|
||||||
pub fn is_available(binary: &str) -> bool {
|
|
||||||
match Command::new(binary).stdout(Stdio::null()).stderr(Stdio::null()).spawn() {
|
|
||||||
Ok(mut child) => {
|
|
||||||
child.kill();
|
|
||||||
|
|
||||||
true
|
|
||||||
},
|
|
||||||
Err(_) => false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
pub fn prettify_bytes(bytes: u64) -> String {
|
|
||||||
if bytes > 1024 * 1024 * 1024 {
|
|
||||||
format!("{:.2} GB", bytes as f64 / 1024.0 / 1024.0 / 1024.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if bytes > 1024 * 1024 {
|
|
||||||
format!("{:.2} MB", bytes as f64 / 1024.0 / 1024.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if bytes > 1024 {
|
|
||||||
format!("{:.2} KB", bytes as f64 / 1024.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
format!("{:.2} B", bytes)
|
|
||||||
}
|
|
||||||
}
|
|
109
src/lib/wine.rs
109
src/lib/wine.rs
|
@ -1,109 +0,0 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use wincompatlib::prelude::*;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref GROUPS: Vec<Group> = vec![
|
|
||||||
Group {
|
|
||||||
title: String::from("Wine-GE-Proton"),
|
|
||||||
subtitle: None,
|
|
||||||
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/wine/wine-ge-proton.json")).unwrap().into_iter().take(12).collect()
|
|
||||||
},
|
|
||||||
Group {
|
|
||||||
title: String::from("GE-Proton"),
|
|
||||||
subtitle: Some(String::from("This version includes its own DXVK builds and you can use DXVK_ASYNC variable")),
|
|
||||||
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/wine/ge-proton.json")).unwrap().into_iter().take(12).collect()
|
|
||||||
},
|
|
||||||
Group {
|
|
||||||
title: String::from("Soda"),
|
|
||||||
subtitle: Some(String::from("New runner based on Valve's Wine, with patches from Proton, TKG and GE. Developed by Bottles")),
|
|
||||||
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/wine/soda.json")).unwrap().into_iter().take(12).collect()
|
|
||||||
},
|
|
||||||
Group {
|
|
||||||
title: String::from("Lutris"),
|
|
||||||
subtitle: None,
|
|
||||||
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/wine/lutris.json")).unwrap().into_iter().take(12).collect()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct List;
|
|
||||||
|
|
||||||
impl List {
|
|
||||||
pub fn get() -> Vec<Group> {
|
|
||||||
GROUPS.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List only downloaded wine versions in some specific folder
|
|
||||||
pub fn list_downloaded<T: Into<PathBuf>>(folder: T) -> std::io::Result<Vec<Version>> {
|
|
||||||
let mut downloaded = Vec::new();
|
|
||||||
|
|
||||||
let list = Self::get();
|
|
||||||
|
|
||||||
for entry in std::fs::read_dir(folder.into())? {
|
|
||||||
let name = entry?.file_name();
|
|
||||||
|
|
||||||
for group in &list {
|
|
||||||
for version in &group.versions {
|
|
||||||
if name == version.name.as_str() {
|
|
||||||
downloaded.push(version.clone());
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloaded.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
|
||||||
|
|
||||||
Ok(downloaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Group {
|
|
||||||
pub title: String,
|
|
||||||
pub subtitle: Option<String>,
|
|
||||||
pub versions: Vec<Version>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Version {
|
|
||||||
pub name: String,
|
|
||||||
pub title: String,
|
|
||||||
pub uri: String,
|
|
||||||
pub files: Files,
|
|
||||||
pub recommended: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Version {
|
|
||||||
pub fn latest() -> Result<Self, serde_json::Error> {
|
|
||||||
Ok(List::get()[0].versions[0].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_downloaded_in<T: Into<PathBuf>>(&self, folder: T) -> bool {
|
|
||||||
folder.into().join(&self.name).exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_wine(&self) -> Wine {
|
|
||||||
Wine::new(
|
|
||||||
&self.files.wine64,
|
|
||||||
None,
|
|
||||||
Some(WineArch::Win64),
|
|
||||||
Some(&self.files.wineboot),
|
|
||||||
Some(&self.files.wineserver),
|
|
||||||
WineLoader::Current
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Files {
|
|
||||||
pub wine: String,
|
|
||||||
pub wine64: String,
|
|
||||||
pub wineserver: String,
|
|
||||||
pub wineboot: String,
|
|
||||||
pub winecfg: String
|
|
||||||
}
|
|
189
src/main.rs
189
src/main.rs
|
@ -1,184 +1,35 @@
|
||||||
use gtk::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use gtk::{CssProvider, StyleContext, STYLE_PROVIDER_PRIORITY_APPLICATION};
|
use anime_launcher_sdk::config;
|
||||||
use gtk::gdk::Display;
|
|
||||||
use gtk::glib;
|
|
||||||
use gtk::glib::clone;
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
|
pub mod i18n;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod lib;
|
|
||||||
|
|
||||||
use ui::*;
|
|
||||||
|
|
||||||
pub const APP_ID: &str = "moe.launcher.an-anime-game-launcher-gtk";
|
|
||||||
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
pub const APP_DEBUG: bool = cfg!(debug_assertions);
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
|
||||||
|
.with_max_level(tracing::Level::TRACE)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
tracing::info!("Starting application");
|
||||||
|
|
||||||
adw::init().expect("Libadwaita initialization failed");
|
adw::init().expect("Libadwaita initialization failed");
|
||||||
|
|
||||||
// Register and include resources
|
// Register and include resources
|
||||||
gtk::gio::resources_register_include!(".assets.gresource")
|
gtk::gio::resources_register_include!("resources.gresource")
|
||||||
.expect("Failed to register resources");
|
.expect("Failed to register resources");
|
||||||
|
|
||||||
// Set application's title
|
// Set application's title
|
||||||
glib::set_application_name("An Anime Game Launcher");
|
gtk::glib::set_application_name("An Anime Game Launcher");
|
||||||
glib::set_program_name(Some("An Anime Game Launcher"));
|
gtk::glib::set_program_name(Some("An Anime Game Launcher"));
|
||||||
|
|
||||||
// Create app
|
// Set UI language
|
||||||
let application = gtk::Application::new(
|
unsafe {
|
||||||
Some(APP_ID),
|
i18n::LANG = config::get().unwrap().launcher.language.parse().unwrap();
|
||||||
Default::default()
|
|
||||||
);
|
|
||||||
|
|
||||||
application.add_main_option(
|
|
||||||
"run-game",
|
|
||||||
glib::Char::from(0),
|
|
||||||
glib::OptionFlags::empty(),
|
|
||||||
glib::OptionArg::None,
|
|
||||||
"Run the game",
|
|
||||||
None
|
|
||||||
);
|
|
||||||
|
|
||||||
application.add_main_option(
|
|
||||||
"just-run-game",
|
|
||||||
glib::Char::from(0),
|
|
||||||
glib::OptionFlags::empty(),
|
|
||||||
glib::OptionArg::None,
|
|
||||||
"Run the game whenever it possible, ignoring updates predownloads",
|
|
||||||
None
|
|
||||||
);
|
|
||||||
|
|
||||||
let run_game = std::rc::Rc::new(std::cell::Cell::new(false));
|
|
||||||
let just_run_game = std::rc::Rc::new(std::cell::Cell::new(false));
|
|
||||||
|
|
||||||
application.connect_handle_local_options(clone!(@strong run_game, @strong just_run_game => move |_, arg| {
|
|
||||||
if arg.contains("just-run-game") {
|
|
||||||
just_run_game.set(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if arg.contains("run-game") {
|
// Run the app
|
||||||
run_game.set(true);
|
let app = RelmApp::new("moe.launcher.an-anime-game-launcher");
|
||||||
}
|
|
||||||
|
app.run::<ui::main::App>(());
|
||||||
-1
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Init app window and show it
|
|
||||||
application.connect_activate(move |app| {
|
|
||||||
// Apply CSS styles to the application
|
|
||||||
let provider = CssProvider::new();
|
|
||||||
|
|
||||||
provider.load_from_data(include_bytes!("../assets/styles.css"));
|
|
||||||
|
|
||||||
StyleContext::add_provider_for_display(
|
|
||||||
&Display::default().expect("Could not connect to a display"),
|
|
||||||
&provider,
|
|
||||||
STYLE_PROVIDER_PRIORITY_APPLICATION
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create default launcher folder if needed
|
|
||||||
let launcher_dir = lib::consts::launcher_dir().expect("Failed to get launcher dir");
|
|
||||||
|
|
||||||
if !launcher_dir.exists() || launcher_dir.join(".first-run").exists() {
|
|
||||||
fs::create_dir_all(&launcher_dir).expect("Failed to create default launcher dir");
|
|
||||||
fs::write(launcher_dir.join(".first-run"), "").expect("Failed to create .first-run file");
|
|
||||||
|
|
||||||
let first_run = FirstRunApp::new(app).expect("Failed to init FirstRunApp");
|
|
||||||
|
|
||||||
first_run.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
let config = lib::config::get().expect("Failed to load config");
|
|
||||||
|
|
||||||
// Create wine builds folder
|
|
||||||
if !Path::new(&config.game.wine.builds).exists() {
|
|
||||||
fs::create_dir_all(config.game.wine.builds)
|
|
||||||
.expect("Failed to create wine builds directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create DXVK builds folder
|
|
||||||
if !Path::new(&config.game.dxvk.builds).exists() {
|
|
||||||
fs::create_dir_all(config.game.dxvk.builds)
|
|
||||||
.expect("Failed to create DXVK builds directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set game edition
|
|
||||||
anime_game_core::genshin::consts::set_game_edition(config.launcher.edition.into());
|
|
||||||
|
|
||||||
// Load main window
|
|
||||||
let main = MainApp::new(app).expect("Failed to init MainApp");
|
|
||||||
|
|
||||||
// Load initial launcher state
|
|
||||||
let awaiter = main.update_state();
|
|
||||||
|
|
||||||
if !run_game.get() && !just_run_game.get() {
|
|
||||||
main.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
use lib::launcher::states::LauncherState;
|
|
||||||
|
|
||||||
let just_run_game = just_run_game.get();
|
|
||||||
|
|
||||||
awaiter.then(move |state| {
|
|
||||||
let mut state = state.as_ref().expect("Failed to load launcher state");
|
|
||||||
|
|
||||||
#[allow(clippy::or_fun_call)]
|
|
||||||
if let LauncherState::PredownloadAvailable { game, voices } = state {
|
|
||||||
if just_run_game {
|
|
||||||
state = &LauncherState::Launch;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if let Ok(config) = lib::config::get() {
|
|
||||||
let mut predownloaded = true;
|
|
||||||
|
|
||||||
let temp = config.launcher.temp.unwrap_or("/tmp".into());
|
|
||||||
|
|
||||||
if !temp.join(game.file_name().unwrap_or(String::from("\0"))).exists() {
|
|
||||||
predownloaded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
for voice in voices {
|
|
||||||
if !temp.join(voice.file_name().unwrap_or(String::from("\0"))).exists() {
|
|
||||||
predownloaded = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if predownloaded {
|
|
||||||
state = &LauncherState::Launch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match state {
|
|
||||||
LauncherState::Launch => {
|
|
||||||
main.update(ui::main::Actions::PerformButtonEvent).unwrap();
|
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => main.show()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Flush config from the memory to the file before closing the app
|
|
||||||
application.connect_shutdown(|_| {
|
|
||||||
lib::config::flush().expect("Failed to save config file");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run app
|
|
||||||
application.run();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::lib::dxvk::Group;
|
|
||||||
use super::dxvk_row::DxvkRow;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DxvkGroup {
|
|
||||||
pub group: Group,
|
|
||||||
pub version_components: Vec<DxvkRow>,
|
|
||||||
|
|
||||||
pub expander_row: adw::ExpanderRow
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DxvkGroup {
|
|
||||||
pub fn new(group: Group) -> Self {
|
|
||||||
let expander_row = adw::ExpanderRow::new();
|
|
||||||
|
|
||||||
expander_row.set_title(&group.title);
|
|
||||||
expander_row.set_subtitle(group.subtitle.as_ref().unwrap_or(&String::new()));
|
|
||||||
|
|
||||||
let mut version_components = Vec::new();
|
|
||||||
|
|
||||||
for version in &group.versions {
|
|
||||||
let component = DxvkRow::new(version.clone());
|
|
||||||
|
|
||||||
expander_row.add_row(&component.row);
|
|
||||||
|
|
||||||
version_components.push(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
group,
|
|
||||||
version_components,
|
|
||||||
expander_row
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_states<T: Into<PathBuf>>(&self, runners_folder: T) {
|
|
||||||
let runners_folder = runners_folder.into();
|
|
||||||
|
|
||||||
for component in &self.version_components {
|
|
||||||
component.update_state(&runners_folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::lib::dxvk::Version;
|
|
||||||
use crate::ui::traits::download_component::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DxvkRow {
|
|
||||||
pub version: Version,
|
|
||||||
|
|
||||||
pub row: adw::ActionRow,
|
|
||||||
pub button: gtk::Button,
|
|
||||||
pub apply_button: gtk::Button,
|
|
||||||
pub progress_bar: gtk::ProgressBar
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DxvkRow {
|
|
||||||
pub fn new(version: Version) -> Self {
|
|
||||||
let row = adw::ActionRow::new();
|
|
||||||
let button = gtk::Button::new();
|
|
||||||
let apply_button = gtk::Button::new();
|
|
||||||
|
|
||||||
row.set_title(&version.version);
|
|
||||||
row.set_visible(version.recommended);
|
|
||||||
|
|
||||||
apply_button.set_icon_name("view-refresh-symbolic");
|
|
||||||
apply_button.set_valign(gtk::Align::Center);
|
|
||||||
apply_button.add_css_class("flat");
|
|
||||||
apply_button.set_tooltip_text(Some("Apply"));
|
|
||||||
apply_button.hide();
|
|
||||||
|
|
||||||
row.add_suffix(&apply_button);
|
|
||||||
|
|
||||||
button.set_icon_name("document-save-symbolic");
|
|
||||||
button.set_valign(gtk::Align::Center);
|
|
||||||
button.add_css_class("flat");
|
|
||||||
|
|
||||||
row.add_suffix(&button);
|
|
||||||
|
|
||||||
let progress_bar = gtk::ProgressBar::new();
|
|
||||||
|
|
||||||
progress_bar.set_text(Some("Downloading: 0%"));
|
|
||||||
progress_bar.set_show_text(true);
|
|
||||||
|
|
||||||
progress_bar.set_width_request(200);
|
|
||||||
progress_bar.set_valign(gtk::Align::Center);
|
|
||||||
progress_bar.hide();
|
|
||||||
|
|
||||||
row.add_suffix(&progress_bar);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
version,
|
|
||||||
row,
|
|
||||||
button,
|
|
||||||
apply_button,
|
|
||||||
progress_bar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_state<T: Into<PathBuf>>(&self, dxvks_folder: T) {
|
|
||||||
if self.is_downloaded(dxvks_folder) {
|
|
||||||
self.button.set_icon_name("user-trash-symbolic");
|
|
||||||
|
|
||||||
self.apply_button.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
self.button.set_icon_name("document-save-symbolic");
|
|
||||||
|
|
||||||
self.apply_button.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply<T: Into<PathBuf>>(&self, dxvks_folder: T, prefix_path: T) -> anyhow::Result<std::process::Output> {
|
|
||||||
self.button.set_sensitive(false);
|
|
||||||
self.apply_button.set_sensitive(false);
|
|
||||||
|
|
||||||
let result = self.version.apply(dxvks_folder, prefix_path);
|
|
||||||
|
|
||||||
self.button.set_sensitive(true);
|
|
||||||
self.apply_button.set_sensitive(true);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DownloadComponent for DxvkRow {
|
|
||||||
fn get_component_path<T: Into<PathBuf>>(&self, installation_path: T) -> PathBuf {
|
|
||||||
installation_path.into().join(&self.version.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_downloading_widgets(&self) -> (gtk::ProgressBar, gtk::Button) {
|
|
||||||
(self.progress_bar.clone(), self.button.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_download_uri(&self) -> String {
|
|
||||||
self.version.uri.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for DxvkRow {}
|
|
||||||
unsafe impl Sync for DxvkRow {}
|
|
|
@ -1,6 +0,0 @@
|
||||||
pub mod wine_group;
|
|
||||||
pub mod wine_row;
|
|
||||||
pub mod dxvk_group;
|
|
||||||
pub mod dxvk_row;
|
|
||||||
pub mod progress_bar;
|
|
||||||
pub mod voiceover_row;
|
|
|
@ -1,95 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
use anime_game_core::prelude::*;
|
|
||||||
|
|
||||||
use crate::lib::prettify_bytes::prettify_bytes;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ProgressUpdateResult {
|
|
||||||
Updated,
|
|
||||||
Error(String, String),
|
|
||||||
Finished
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct ProgressBar {
|
|
||||||
pub progress_bar: gtk::ProgressBar,
|
|
||||||
pub default_group: adw::PreferencesGroup,
|
|
||||||
pub progress_bar_group: adw::PreferencesGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProgressBar {
|
|
||||||
pub fn new(progress_bar: gtk::ProgressBar, default_group: adw::PreferencesGroup, progress_bar_group: adw::PreferencesGroup) -> Self {
|
|
||||||
Self {
|
|
||||||
progress_bar,
|
|
||||||
default_group,
|
|
||||||
progress_bar_group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show(&self) {
|
|
||||||
self.progress_bar.set_text(None);
|
|
||||||
self.progress_bar.set_fraction(0.0);
|
|
||||||
|
|
||||||
self.default_group.hide();
|
|
||||||
self.progress_bar_group.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hide(&self) {
|
|
||||||
self.default_group.show();
|
|
||||||
self.progress_bar_group.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&self, fraction: f64, text: Option<&str>) {
|
|
||||||
self.progress_bar.set_fraction(fraction);
|
|
||||||
self.progress_bar.set_text(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_from_state(&self, state: InstallerUpdate) -> ProgressUpdateResult {
|
|
||||||
match state {
|
|
||||||
InstallerUpdate::CheckingFreeSpace(_) => self.progress_bar.set_text(Some("Checking free space...")),
|
|
||||||
InstallerUpdate::DownloadingStarted(_) => (),
|
|
||||||
|
|
||||||
InstallerUpdate::DownloadingProgress(curr, total) => {
|
|
||||||
let progress = curr as f64 / total as f64;
|
|
||||||
|
|
||||||
self.update(progress, Some(&format!(
|
|
||||||
"Downloading: {:.2}% ({} of {})",
|
|
||||||
progress * 100.0,
|
|
||||||
prettify_bytes(curr),
|
|
||||||
prettify_bytes(total)
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::UnpackingProgress(curr, total) => {
|
|
||||||
let progress = curr as f64 / total as f64;
|
|
||||||
|
|
||||||
self.update(progress, Some(&format!(
|
|
||||||
"Unpacking: {:.2}% ({} of {})",
|
|
||||||
progress * 100.0,
|
|
||||||
prettify_bytes(curr),
|
|
||||||
prettify_bytes(total)
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::DownloadingFinished => (),
|
|
||||||
InstallerUpdate::UnpackingStarted(_) => (),
|
|
||||||
|
|
||||||
InstallerUpdate::DownloadingError(err) => {
|
|
||||||
let err: std::io::Error = err.into();
|
|
||||||
|
|
||||||
return ProgressUpdateResult::Error(String::from("Failed to download"), err.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::UnpackingError(err) => return ProgressUpdateResult::Error(String::from("Failed to unpack"), err),
|
|
||||||
InstallerUpdate::UnpackingFinished => return ProgressUpdateResult::Finished
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressUpdateResult::Updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for ProgressBar {}
|
|
||||||
unsafe impl Sync for ProgressBar {}
|
|
|
@ -1,52 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use anime_game_core::genshin::voice_data::package::VoicePackage;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct VoiceoverRow {
|
|
||||||
pub package: VoicePackage,
|
|
||||||
|
|
||||||
pub row: adw::ActionRow,
|
|
||||||
pub button: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VoiceoverRow {
|
|
||||||
pub fn new(package: VoicePackage) -> Self {
|
|
||||||
let row = adw::ActionRow::new();
|
|
||||||
let button = gtk::Button::new();
|
|
||||||
|
|
||||||
row.set_title(package.locale().to_name());
|
|
||||||
|
|
||||||
button.set_icon_name("document-save-symbolic");
|
|
||||||
button.set_valign(gtk::Align::Center);
|
|
||||||
button.add_css_class("flat");
|
|
||||||
|
|
||||||
row.add_suffix(&button);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
package,
|
|
||||||
row,
|
|
||||||
button
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_state<T: Into<PathBuf>>(&self, game_path: T) {
|
|
||||||
if self.is_downloaded(game_path) {
|
|
||||||
self.button.set_icon_name("user-trash-symbolic");
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
self.button.set_icon_name("document-save-symbolic");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_downloaded<T: Into<PathBuf>>(&self, game_path: T) -> bool {
|
|
||||||
self.package.is_installed_in(game_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for VoiceoverRow {}
|
|
||||||
unsafe impl Sync for VoiceoverRow {}
|
|
|
@ -1,47 +0,0 @@
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::lib::wine::Group;
|
|
||||||
use super::wine_row::WineRow;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WineGroup {
|
|
||||||
pub group: Group,
|
|
||||||
pub version_components: Vec<WineRow>,
|
|
||||||
|
|
||||||
pub expander_row: adw::ExpanderRow
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WineGroup {
|
|
||||||
pub fn new(group: Group) -> Self {
|
|
||||||
let expander_row = adw::ExpanderRow::new();
|
|
||||||
|
|
||||||
expander_row.set_title(&group.title);
|
|
||||||
expander_row.set_subtitle(group.subtitle.as_ref().unwrap_or(&String::new()));
|
|
||||||
|
|
||||||
let mut version_components = Vec::new();
|
|
||||||
|
|
||||||
for version in &group.versions {
|
|
||||||
let component = WineRow::new(version.clone());
|
|
||||||
|
|
||||||
expander_row.add_row(&component.row);
|
|
||||||
|
|
||||||
version_components.push(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
group,
|
|
||||||
version_components,
|
|
||||||
expander_row
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_states<T: Into<PathBuf>>(&self, runners_folder: T) {
|
|
||||||
let runners_folder = runners_folder.into();
|
|
||||||
|
|
||||||
for component in &self.version_components {
|
|
||||||
component.update_state(&runners_folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::lib::wine::Version;
|
|
||||||
use crate::ui::traits::download_component::*;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WineRow {
|
|
||||||
pub version: Version,
|
|
||||||
|
|
||||||
pub row: adw::ActionRow,
|
|
||||||
pub button: gtk::Button,
|
|
||||||
pub progress_bar: gtk::ProgressBar
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WineRow {
|
|
||||||
pub fn new(version: Version) -> Self {
|
|
||||||
let row = adw::ActionRow::new();
|
|
||||||
let button = gtk::Button::new();
|
|
||||||
|
|
||||||
row.set_title(&version.title);
|
|
||||||
row.set_visible(version.recommended);
|
|
||||||
|
|
||||||
button.set_icon_name("document-save-symbolic");
|
|
||||||
button.set_valign(gtk::Align::Center);
|
|
||||||
button.add_css_class("flat");
|
|
||||||
|
|
||||||
row.add_suffix(&button);
|
|
||||||
|
|
||||||
let progress_bar = gtk::ProgressBar::new();
|
|
||||||
|
|
||||||
progress_bar.set_text(Some("Downloading: 0%"));
|
|
||||||
progress_bar.set_show_text(true);
|
|
||||||
|
|
||||||
progress_bar.set_width_request(200);
|
|
||||||
progress_bar.set_valign(gtk::Align::Center);
|
|
||||||
progress_bar.set_visible(false);
|
|
||||||
|
|
||||||
row.add_suffix(&progress_bar);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
version,
|
|
||||||
row,
|
|
||||||
button,
|
|
||||||
progress_bar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_state<T: Into<PathBuf>>(&self, runners_folder: T) {
|
|
||||||
if self.is_downloaded(runners_folder) {
|
|
||||||
self.button.set_icon_name("user-trash-symbolic");
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
self.button.set_icon_name("document-save-symbolic");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DownloadComponent for WineRow {
|
|
||||||
fn get_component_path<T: Into<PathBuf>>(&self, installation_path: T) -> PathBuf {
|
|
||||||
installation_path.into().join(&self.version.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_downloading_widgets(&self) -> (gtk::ProgressBar, gtk::Button) {
|
|
||||||
(self.progress_bar.clone(), self.button.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_download_uri(&self) -> String {
|
|
||||||
self.version.uri.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for WineRow {}
|
|
||||||
unsafe impl Sync for WineRow {}
|
|
|
@ -1,121 +0,0 @@
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
use gtk::glib::clone;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use wait_not_await::Await;
|
|
||||||
|
|
||||||
use crate::lib::config;
|
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
pub fn choose_dir(current_folder: String) -> Await<Option<String>> {
|
|
||||||
let dialogue = rfd::FileDialog::new()
|
|
||||||
.set_directory(current_folder);
|
|
||||||
|
|
||||||
let (sender, receiver) = std::sync::mpsc::channel();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
sender.send(dialogue.pick_folder()).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
Await::new(move || {
|
|
||||||
match receiver.recv() {
|
|
||||||
Ok(Some(path)) => Some(path.to_string_lossy().to_string()),
|
|
||||||
Ok(None) => None,
|
|
||||||
Err(_) => None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub window: gtk::Window,
|
|
||||||
pub page: gtk::Box,
|
|
||||||
|
|
||||||
pub runners_folder: adw::ActionRow,
|
|
||||||
pub dxvk_folder: adw::ActionRow,
|
|
||||||
pub prefix_folder: adw::ActionRow,
|
|
||||||
pub game_folder: adw::ActionRow,
|
|
||||||
pub patch_folder: adw::ActionRow,
|
|
||||||
pub temp_folder: adw::ActionRow,
|
|
||||||
|
|
||||||
pub continue_button: gtk::Button,
|
|
||||||
pub exit_button: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new(window: gtk::Window) -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/default_paths.ui");
|
|
||||||
|
|
||||||
let result = Self {
|
|
||||||
window,
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
|
|
||||||
runners_folder: get_object(&builder, "runners_folder")?,
|
|
||||||
dxvk_folder: get_object(&builder, "dxvk_folder")?,
|
|
||||||
prefix_folder: get_object(&builder, "prefix_folder")?,
|
|
||||||
game_folder: get_object(&builder, "game_folder")?,
|
|
||||||
patch_folder: get_object(&builder, "patch_folder")?,
|
|
||||||
temp_folder: get_object(&builder, "temp_folder")?,
|
|
||||||
|
|
||||||
continue_button: get_object(&builder, "continue_button")?,
|
|
||||||
exit_button: get_object(&builder, "exit_button")?
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
// Add paths to subtitles
|
|
||||||
result.runners_folder.set_subtitle(config.game.wine.builds.to_str().unwrap());
|
|
||||||
result.dxvk_folder.set_subtitle(config.game.dxvk.builds.to_str().unwrap());
|
|
||||||
result.prefix_folder.set_subtitle(config.game.wine.prefix.to_str().unwrap());
|
|
||||||
result.game_folder.set_subtitle(config.game.path.to_str().unwrap());
|
|
||||||
result.patch_folder.set_subtitle(config.patch.path.to_str().unwrap());
|
|
||||||
result.temp_folder.set_subtitle(&match config.launcher.temp {
|
|
||||||
Some(temp) => temp.to_string_lossy().to_string(),
|
|
||||||
None => String::from("/tmp")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect path selection events
|
|
||||||
result.connect_activated(&result.runners_folder);
|
|
||||||
result.connect_activated(&result.dxvk_folder);
|
|
||||||
result.connect_activated(&result.prefix_folder);
|
|
||||||
result.connect_activated(&result.game_folder);
|
|
||||||
result.connect_activated(&result.patch_folder);
|
|
||||||
result.connect_activated(&result.temp_folder);
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connect_activated(&self, row: &adw::ActionRow) {
|
|
||||||
row.connect_activated(clone!(@strong self.window as window => move |row| {
|
|
||||||
let (sender, receiver) = glib::MainContext::channel::<String>(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
choose_dir(row.subtitle().unwrap().to_string()).then(move |path| {
|
|
||||||
if let Some(path) = path {
|
|
||||||
sender.send(path.clone()).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let row = row.clone();
|
|
||||||
|
|
||||||
receiver.attach(None, move |path| {
|
|
||||||
row.set_subtitle(&path);
|
|
||||||
|
|
||||||
glib::Continue(false)
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_config(&self, mut config: config::Config) -> config::Config {
|
|
||||||
config.game.wine.builds = PathBuf::from(self.runners_folder.subtitle().unwrap().to_string());
|
|
||||||
config.game.dxvk.builds = PathBuf::from(self.dxvk_folder.subtitle().unwrap().to_string());
|
|
||||||
config.game.wine.prefix = PathBuf::from(self.prefix_folder.subtitle().unwrap().to_string());
|
|
||||||
config.game.path = PathBuf::from(self.game_folder.subtitle().unwrap().to_string());
|
|
||||||
config.patch.path = PathBuf::from(self.patch_folder.subtitle().unwrap().to_string());
|
|
||||||
config.launcher.temp = Some(PathBuf::from(self.temp_folder.subtitle().unwrap().to_string()));
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub page: gtk::Box,
|
|
||||||
|
|
||||||
pub pkg_pacman: gtk::Box,
|
|
||||||
pub pkg_apt: gtk::Box,
|
|
||||||
pub pkg_dnf: gtk::Box,
|
|
||||||
|
|
||||||
pub check_button: gtk::Button,
|
|
||||||
pub exit_button: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/dependencies.ui");
|
|
||||||
|
|
||||||
let result = Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
|
|
||||||
pkg_pacman: get_object(&builder, "pkg_pacman")?,
|
|
||||||
pkg_apt: get_object(&builder, "pkg_apt")?,
|
|
||||||
pkg_dnf: get_object(&builder, "pkg_dnf")?,
|
|
||||||
|
|
||||||
check_button: get_object(&builder, "check_button")?,
|
|
||||||
exit_button: get_object(&builder, "exit_button")?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Decide which packaging format used in system
|
|
||||||
match Command::new("pacman").stdout(Stdio::null()).spawn() {
|
|
||||||
Ok(_) => result.pkg_pacman.show(),
|
|
||||||
|
|
||||||
Err(_) => match Command::new("apt").stdout(Stdio::null()).spawn() {
|
|
||||||
Ok(_) => result.pkg_apt.show(),
|
|
||||||
|
|
||||||
Err(_) => match Command::new("dnf").stdout(Stdio::null()).spawn() {
|
|
||||||
Ok(_) => result.pkg_dnf.show(),
|
|
||||||
|
|
||||||
Err(_) => {
|
|
||||||
result.pkg_pacman.show();
|
|
||||||
result.pkg_apt.show();
|
|
||||||
result.pkg_dnf.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use crate::lib::wine::{Version as WineVersion, List as WineList};
|
|
||||||
use crate::lib::dxvk::{Version as DxvkVersion, List as DxvkList};
|
|
||||||
|
|
||||||
use crate::ui::*;
|
|
||||||
use crate::ui::components::progress_bar::ProgressBar;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub page: gtk::Box,
|
|
||||||
|
|
||||||
pub wine_version: adw::ComboRow,
|
|
||||||
pub dxvk_version: adw::ComboRow,
|
|
||||||
|
|
||||||
pub download_button: gtk::Button,
|
|
||||||
pub exit_button: gtk::Button,
|
|
||||||
|
|
||||||
pub progress_bar: ProgressBar,
|
|
||||||
|
|
||||||
pub wine_versions: Vec<WineVersion>,
|
|
||||||
pub dxvk_versions: Vec<DxvkVersion>,
|
|
||||||
|
|
||||||
system_wine_available: bool
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/download_components.ui");
|
|
||||||
|
|
||||||
let mut result = Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
|
|
||||||
wine_version: get_object(&builder, "wine_version")?,
|
|
||||||
dxvk_version: get_object(&builder, "dxvk_version")?,
|
|
||||||
|
|
||||||
download_button: get_object(&builder, "download_button")?,
|
|
||||||
exit_button: get_object(&builder, "exit_button")?,
|
|
||||||
|
|
||||||
progress_bar: ProgressBar::new(
|
|
||||||
get_object(&builder, "progress_bar")?,
|
|
||||||
get_object(&builder, "buttons_group")?,
|
|
||||||
get_object(&builder, "progress_bar_group")?
|
|
||||||
),
|
|
||||||
|
|
||||||
wine_versions: Vec::new(),
|
|
||||||
dxvk_versions: Vec::new(),
|
|
||||||
|
|
||||||
system_wine_available: crate::lib::is_available("wine64")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add wine versions
|
|
||||||
let model = gtk::StringList::new(&[]);
|
|
||||||
|
|
||||||
if result.system_wine_available {
|
|
||||||
model.append("System");
|
|
||||||
}
|
|
||||||
|
|
||||||
for version in &WineList::get()[0].versions {
|
|
||||||
if version.recommended {
|
|
||||||
model.append(&version.title);
|
|
||||||
|
|
||||||
result.wine_versions.push(version.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.wine_version.set_model(Some(&model));
|
|
||||||
|
|
||||||
// We're not recommending user to use system wine
|
|
||||||
// and suggest to download some wine build better for gaming
|
|
||||||
if result.system_wine_available {
|
|
||||||
result.wine_version.set_selected(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add DXVK versions
|
|
||||||
let model = gtk::StringList::new(&[]);
|
|
||||||
|
|
||||||
for version in &DxvkList::get()[0].versions {
|
|
||||||
if version.recommended {
|
|
||||||
model.append(&version.version);
|
|
||||||
|
|
||||||
result.dxvk_versions.push(version.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.dxvk_version.set_model(Some(&model));
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get selected wine version
|
|
||||||
///
|
|
||||||
/// `None` means `System`
|
|
||||||
pub fn get_wine_version(&self) -> Option<WineVersion> {
|
|
||||||
if self.system_wine_available {
|
|
||||||
match self.wine_version.selected() {
|
|
||||||
0 => None,
|
|
||||||
i => Some(self.wine_versions[i as usize - 1].clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
Some(self.wine_versions[self.wine_version.selected() as usize].clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dxvk_version(&self) -> &DxvkVersion {
|
|
||||||
&self.dxvk_versions[self.dxvk_version.selected() as usize]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub page: gtk::Box,
|
|
||||||
pub restart_button: gtk::Button,
|
|
||||||
pub exit_button: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/finish.ui");
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
restart_button: get_object(&builder, "restart_button")?,
|
|
||||||
exit_button: get_object(&builder, "exit_button")?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,473 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
use gtk::glib::clone;
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::cell::Cell;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use anime_game_core::prelude::*;
|
|
||||||
|
|
||||||
use wincompatlib::prelude::*;
|
|
||||||
|
|
||||||
mod welcome;
|
|
||||||
mod dependencies;
|
|
||||||
mod tos_warning;
|
|
||||||
mod default_paths;
|
|
||||||
mod voice_packages;
|
|
||||||
mod download_components;
|
|
||||||
mod finish;
|
|
||||||
|
|
||||||
use crate::ui::*;
|
|
||||||
use crate::ui::traits::prelude::*;
|
|
||||||
use crate::ui::components::progress_bar::*;
|
|
||||||
|
|
||||||
use crate::lib;
|
|
||||||
use crate::lib::config;
|
|
||||||
|
|
||||||
/// This structure is used to describe widgets used in application
|
|
||||||
///
|
|
||||||
/// `AppWidgets::default` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets
|
|
||||||
///
|
|
||||||
/// This function does not implement events
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AppWidgets {
|
|
||||||
pub window: adw::ApplicationWindow,
|
|
||||||
pub toast_overlay: adw::ToastOverlay,
|
|
||||||
pub carousel: adw::Carousel,
|
|
||||||
|
|
||||||
pub welcome: welcome::Page,
|
|
||||||
pub dependencies: dependencies::Page,
|
|
||||||
pub tos_warning: tos_warning::Page,
|
|
||||||
pub default_paths: default_paths::Page,
|
|
||||||
pub voice_packages: voice_packages::Page,
|
|
||||||
pub download_components: download_components::Page,
|
|
||||||
pub finish: finish::Page
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppWidgets {
|
|
||||||
pub fn try_get() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run.ui");
|
|
||||||
|
|
||||||
let result = Self {
|
|
||||||
window: get_object(&builder, "window")?,
|
|
||||||
toast_overlay: get_object(&builder, "toast_overlay")?,
|
|
||||||
carousel: get_object(&builder, "carousel")?,
|
|
||||||
|
|
||||||
welcome: welcome::Page::new()?,
|
|
||||||
dependencies: dependencies::Page::new()?,
|
|
||||||
tos_warning: tos_warning::Page::new()?,
|
|
||||||
default_paths: default_paths::Page::new(get_object(&builder, "window")?)?,
|
|
||||||
voice_packages: voice_packages::Page::new()?,
|
|
||||||
download_components: download_components::Page::new()?,
|
|
||||||
finish: finish::Page::new()?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add pages to carousel
|
|
||||||
result.carousel.append(&result.welcome.page);
|
|
||||||
result.carousel.append(&result.dependencies.page);
|
|
||||||
result.carousel.append(&result.tos_warning.page);
|
|
||||||
result.carousel.append(&result.default_paths.page);
|
|
||||||
result.carousel.append(&result.voice_packages.page);
|
|
||||||
result.carousel.append(&result.download_components.page);
|
|
||||||
result.carousel.append(&result.finish.page);
|
|
||||||
|
|
||||||
// Set devel style to ApplicationWindow if it's debug mode
|
|
||||||
if crate::APP_DEBUG {
|
|
||||||
result.window.add_css_class("devel");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This enum is used to describe an action inside of this application
|
|
||||||
///
|
|
||||||
/// It may be helpful if you want to add the same event for several widgets, or call an action inside of another action
|
|
||||||
///
|
|
||||||
/// Has to implement glib::Downgrade` trait
|
|
||||||
#[derive(Debug, glib::Downgrade)]
|
|
||||||
pub enum Actions {
|
|
||||||
WelcomeContinue,
|
|
||||||
WelcomeAdvanced,
|
|
||||||
DependenciesContinue,
|
|
||||||
TosWarningContinue,
|
|
||||||
DefaultPathsContinue,
|
|
||||||
VoicePackagesContinue,
|
|
||||||
DownloadComponents,
|
|
||||||
DownloadComponentsContinue,
|
|
||||||
Restart,
|
|
||||||
Exit,
|
|
||||||
Toast(Rc<(String, String)>)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actions {
|
|
||||||
pub fn into_fn<T: gtk::glib::IsA<gtk::Widget>>(&self, app: &App) -> Box<dyn Fn(&T)> {
|
|
||||||
Box::new(clone!(@weak self as action, @strong app => move |_| {
|
|
||||||
app.update(action).unwrap();
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main application structure
|
|
||||||
///
|
|
||||||
/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets
|
|
||||||
///
|
|
||||||
/// `Rc<Cell<Values>>` means this:
|
|
||||||
/// - `Rc` addeds ability to reference the same value from various clones of the structure.
|
|
||||||
/// This will guarantee us that inner `Cell<Values>` is the same for all the `App::clone()` values
|
|
||||||
/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference.
|
|
||||||
///
|
|
||||||
/// So we have a shared reference to some value that can be changed without mutable reference.
|
|
||||||
/// That's what we need and what we use in `App::update` method
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct App {
|
|
||||||
widgets: AppWidgets,
|
|
||||||
actions: Rc<Cell<Option<glib::Sender<Actions>>>>,
|
|
||||||
advanced: Rc<Cell<bool>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
/// Create new application
|
|
||||||
pub fn new(app: >k::Application) -> anyhow::Result<Self> {
|
|
||||||
// Get default widgets from ui file and add events to them
|
|
||||||
let result = Self {
|
|
||||||
widgets: AppWidgets::try_get()?,
|
|
||||||
actions: Default::default(),
|
|
||||||
advanced: Default::default()
|
|
||||||
}.init_events().init_actions();
|
|
||||||
|
|
||||||
// Bind app to the window
|
|
||||||
result.widgets.window.set_application(Some(app));
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add default events and values to the widgets
|
|
||||||
fn init_events(self) -> Self {
|
|
||||||
self.widgets.welcome.continue_button.connect_clicked(Actions::WelcomeContinue.into_fn(&self));
|
|
||||||
self.widgets.tos_warning.continue_button.connect_clicked(Actions::TosWarningContinue.into_fn(&self));
|
|
||||||
self.widgets.default_paths.continue_button.connect_clicked(Actions::DefaultPathsContinue.into_fn(&self));
|
|
||||||
self.widgets.dependencies.check_button.connect_clicked(Actions::DependenciesContinue.into_fn(&self));
|
|
||||||
self.widgets.voice_packages.continue_button.connect_clicked(Actions::VoicePackagesContinue.into_fn(&self));
|
|
||||||
|
|
||||||
self.widgets.welcome.advanced_button.connect_clicked(Actions::WelcomeAdvanced.into_fn(&self));
|
|
||||||
self.widgets.download_components.download_button.connect_clicked(Actions::DownloadComponents.into_fn(&self));
|
|
||||||
|
|
||||||
self.widgets.dependencies.exit_button.connect_clicked(Actions::Exit.into_fn(&self));
|
|
||||||
self.widgets.tos_warning.exit_button.connect_clicked(Actions::Exit.into_fn(&self));
|
|
||||||
self.widgets.default_paths.exit_button.connect_clicked(Actions::Exit.into_fn(&self));
|
|
||||||
self.widgets.voice_packages.exit_button.connect_clicked(Actions::Exit.into_fn(&self));
|
|
||||||
self.widgets.download_components.exit_button.connect_clicked(Actions::Exit.into_fn(&self));
|
|
||||||
self.widgets.finish.exit_button.connect_clicked(Actions::Exit.into_fn(&self));
|
|
||||||
|
|
||||||
self.widgets.finish.restart_button.connect_clicked(Actions::Restart.into_fn(&self));
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add actions processors
|
|
||||||
///
|
|
||||||
/// Changes will happen in the main thread so you can call `update` method from separate thread
|
|
||||||
pub fn init_actions(self) -> Self {
|
|
||||||
let (sender, receiver) = glib::MainContext::channel::<Actions>(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
let this = self.clone();
|
|
||||||
|
|
||||||
receiver.attach(None, move |action| {
|
|
||||||
// Some debug output
|
|
||||||
println!("[update] action: {:?}", &action);
|
|
||||||
|
|
||||||
match action {
|
|
||||||
Actions::WelcomeContinue => {
|
|
||||||
this.widgets.carousel.scroll_to({
|
|
||||||
if lib::is_available("git") && lib::is_available("xdelta3") {
|
|
||||||
&this.widgets.tos_warning.page
|
|
||||||
} else {
|
|
||||||
&this.widgets.dependencies.page
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::WelcomeAdvanced => {
|
|
||||||
this.advanced.set(true);
|
|
||||||
|
|
||||||
this.update(Actions::WelcomeContinue).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::DependenciesContinue => {
|
|
||||||
let mut installed = true;
|
|
||||||
|
|
||||||
for package in ["git", "xdelta3"] {
|
|
||||||
if !lib::is_available(package) {
|
|
||||||
installed = false;
|
|
||||||
|
|
||||||
this.toast(format!("Package {package} is not installed"), "");
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if installed {
|
|
||||||
this.widgets.carousel.scroll_to(&this.widgets.tos_warning.page, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::TosWarningContinue => {
|
|
||||||
this.widgets.carousel.scroll_to({
|
|
||||||
if this.advanced.get() {
|
|
||||||
&this.widgets.default_paths.page
|
|
||||||
} else {
|
|
||||||
&this.widgets.voice_packages.page
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::DefaultPathsContinue => {
|
|
||||||
config::update_raw(this.widgets.default_paths.update_config(config::get().unwrap())).unwrap();
|
|
||||||
|
|
||||||
this.widgets.carousel.scroll_to(&this.widgets.voice_packages.page, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::VoicePackagesContinue => {
|
|
||||||
config::update_raw(this.widgets.voice_packages.update_config(config::get().unwrap())).unwrap();
|
|
||||||
|
|
||||||
this.widgets.carousel.scroll_to(&this.widgets.download_components.page, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::DownloadComponents => {
|
|
||||||
this.widgets.download_components.wine_version.set_sensitive(false);
|
|
||||||
this.widgets.download_components.dxvk_version.set_sensitive(false);
|
|
||||||
|
|
||||||
this.widgets.download_components.progress_bar.show();
|
|
||||||
|
|
||||||
let (sender_wine, receiver_wine) = glib::MainContext::channel::<InstallerUpdate>(glib::PRIORITY_DEFAULT);
|
|
||||||
let (sender_dxvk, receiver_dxvk) = glib::MainContext::channel::<InstallerUpdate>(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
let progress_bar = this.widgets.download_components.progress_bar.clone();
|
|
||||||
|
|
||||||
let wine_version = this.widgets.download_components.get_wine_version();
|
|
||||||
let dxvk_version = this.widgets.download_components.get_dxvk_version().clone();
|
|
||||||
|
|
||||||
// Prepare wine downloader
|
|
||||||
if let Some(wine_version) = &wine_version {
|
|
||||||
let wine_version_copy = wine_version.clone();
|
|
||||||
let this_copy = this.clone();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let config = config::get().unwrap();
|
|
||||||
|
|
||||||
match Installer::new(&wine_version_copy.uri) {
|
|
||||||
Ok(mut installer) => {
|
|
||||||
if let Some(temp_folder) = config.launcher.temp {
|
|
||||||
installer.temp_folder = temp_folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
installer.downloader
|
|
||||||
.set_downloading_speed(config.launcher.speed_limit)
|
|
||||||
.expect("Failed to set downloading speed limit");
|
|
||||||
|
|
||||||
// Download wine
|
|
||||||
#[allow(unused_must_use)]
|
|
||||||
installer.install(&config.game.wine.builds, move |state| {
|
|
||||||
sender_wine.send(state);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
this_copy.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to init wine downloader"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
sender_wine.send(InstallerUpdate::UnpackingFinished).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display wine downloading progress
|
|
||||||
let progress_bar_copy = progress_bar.clone();
|
|
||||||
let dxvk_version_copy = dxvk_version.clone();
|
|
||||||
|
|
||||||
let this_copy = this.clone();
|
|
||||||
|
|
||||||
receiver_wine.attach(None, move |state| {
|
|
||||||
match progress_bar_copy.update_from_state(state) {
|
|
||||||
ProgressUpdateResult::Updated => (),
|
|
||||||
|
|
||||||
ProgressUpdateResult::Error(msg, err) => {
|
|
||||||
this_copy.toast(msg, err);
|
|
||||||
},
|
|
||||||
|
|
||||||
ProgressUpdateResult::Finished => {
|
|
||||||
let mut config = config::get().unwrap();
|
|
||||||
|
|
||||||
// Update wine config
|
|
||||||
if let Some(wine_version) = &wine_version {
|
|
||||||
config.game.wine.selected = Some(wine_version.name.clone());
|
|
||||||
|
|
||||||
config::update_raw(config.clone()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create wine prefix
|
|
||||||
let this = this_copy.clone();
|
|
||||||
let dxvk_version = dxvk_version_copy.clone();
|
|
||||||
let sender_dxvk = sender_dxvk.clone();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let wine = config.try_get_wine_executable()
|
|
||||||
.expect("None of wine builds are available");
|
|
||||||
|
|
||||||
let wine = Wine::from_binary(wine)
|
|
||||||
.with_loader(WineLoader::Current)
|
|
||||||
.with_arch(WineArch::Win64);
|
|
||||||
|
|
||||||
match wine.update_prefix(&config.game.wine.prefix) {
|
|
||||||
Ok(output) => {
|
|
||||||
println!("Wine prefix created:\n{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
|
|
||||||
// Prepare DXVK downloader
|
|
||||||
match Installer::new(&dxvk_version.uri) {
|
|
||||||
Ok(mut installer) => {
|
|
||||||
if let Some(temp_folder) = config.launcher.temp {
|
|
||||||
installer.temp_folder = temp_folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
installer.downloader
|
|
||||||
.set_downloading_speed(config.launcher.speed_limit)
|
|
||||||
.expect("Failed to set downloading speed limit");
|
|
||||||
|
|
||||||
// Download DXVK
|
|
||||||
#[allow(unused_must_use)]
|
|
||||||
installer.install(&config.game.dxvk.builds, move |state| {
|
|
||||||
sender_dxvk.send(state);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to init DXVK downloader"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to create wine prefix"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return glib::Continue(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::Continue(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Display DXVK downloading progress
|
|
||||||
let this = this.clone();
|
|
||||||
|
|
||||||
receiver_dxvk.attach(None, move |state| {
|
|
||||||
match progress_bar.update_from_state(state) {
|
|
||||||
ProgressUpdateResult::Updated => (),
|
|
||||||
|
|
||||||
ProgressUpdateResult::Error(msg, err) => {
|
|
||||||
this.toast(msg, err);
|
|
||||||
},
|
|
||||||
|
|
||||||
ProgressUpdateResult::Finished => {
|
|
||||||
let config = config::get().unwrap();
|
|
||||||
|
|
||||||
// Apply DXVK
|
|
||||||
let this = this.clone();
|
|
||||||
let dxvk_version = dxvk_version.clone();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
match dxvk_version.apply(&config.game.dxvk.builds, &config.game.wine.prefix) {
|
|
||||||
Ok(output) => {
|
|
||||||
println!("Applied DXVK:\n\n{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
|
|
||||||
// Remove .first-run file
|
|
||||||
let launcher_dir = crate::lib::consts::launcher_dir().unwrap();
|
|
||||||
|
|
||||||
std::fs::remove_file(launcher_dir.join(".first-run")).unwrap();
|
|
||||||
|
|
||||||
// Show next page
|
|
||||||
this.update(Actions::DownloadComponentsContinue).unwrap();
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to apply DXVK"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return glib::Continue(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::Continue(true)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::DownloadComponentsContinue => {
|
|
||||||
this.widgets.carousel.scroll_to(&this.widgets.finish.page, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::Restart => {
|
|
||||||
Command::new(std::env::current_exe().unwrap()).spawn().unwrap();
|
|
||||||
|
|
||||||
this.widgets.window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::Exit => {
|
|
||||||
this.widgets.window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::Toast(toast) => {
|
|
||||||
let (msg, err) = (toast.0.clone(), toast.1.clone());
|
|
||||||
|
|
||||||
this.toast(msg, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::Continue(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.actions.set(Some(sender));
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update widgets state by calling some action
|
|
||||||
pub fn update(&self, action: Actions) -> Result<(), std::sync::mpsc::SendError<Actions>> {
|
|
||||||
let actions = self.actions.take();
|
|
||||||
|
|
||||||
let result = match &actions {
|
|
||||||
Some(sender) => Ok(sender.send(action)?),
|
|
||||||
None => Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
self.actions.set(actions);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show application window
|
|
||||||
pub fn show(&self) {
|
|
||||||
self.widgets.window.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Toast for App {
|
|
||||||
fn get_toast_widgets(&self) -> (adw::ApplicationWindow, adw::ToastOverlay) {
|
|
||||||
(self.widgets.window.clone(), self.widgets.toast_overlay.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for App {}
|
|
||||||
unsafe impl Sync for App {}
|
|
|
@ -1,20 +0,0 @@
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub page: gtk::Box,
|
|
||||||
pub continue_button: gtk::Button,
|
|
||||||
pub exit_button: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/tos_warning.ui");
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
continue_button: get_object(&builder, "continue_button")?,
|
|
||||||
exit_button: get_object(&builder, "exit_button")?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use anime_game_core::genshin::voice_data::prelude::*;
|
|
||||||
|
|
||||||
use crate::lib::config;
|
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub page: gtk::Box,
|
|
||||||
pub voice_packages_group: adw::PreferencesGroup,
|
|
||||||
|
|
||||||
pub continue_button: gtk::Button,
|
|
||||||
pub exit_button: gtk::Button,
|
|
||||||
|
|
||||||
pub voice_packages: Vec<(VoiceLocale, adw::ActionRow, gtk::Switch)>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/voice_packages.ui");
|
|
||||||
|
|
||||||
let mut result = Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
voice_packages_group: get_object(&builder, "voice_packages_group")?,
|
|
||||||
|
|
||||||
continue_button: get_object(&builder, "continue_button")?,
|
|
||||||
exit_button: get_object(&builder, "exit_button")?,
|
|
||||||
|
|
||||||
voice_packages: Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut packages = Vec::new();
|
|
||||||
|
|
||||||
for package in VoicePackage::list_latest().expect("Failed to list voice packages") {
|
|
||||||
let row = adw::ActionRow::new();
|
|
||||||
let switch = gtk::Switch::new();
|
|
||||||
|
|
||||||
row.set_title(package.locale().to_name());
|
|
||||||
switch.set_valign(gtk::Align::Center);
|
|
||||||
|
|
||||||
row.add_suffix(&switch);
|
|
||||||
|
|
||||||
result.voice_packages_group.add(&row);
|
|
||||||
|
|
||||||
packages.push((package.locale(), row, switch));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(config) = config::get() {
|
|
||||||
for voice in config.game.voices {
|
|
||||||
if let Some(voice) = VoiceLocale::from_str(voice) {
|
|
||||||
for (locale, _, switcher) in &packages {
|
|
||||||
if voice == *locale {
|
|
||||||
switcher.set_state(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.voice_packages = packages;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_config(&self, mut config: config::Config) -> config::Config {
|
|
||||||
let mut voices = Vec::new();
|
|
||||||
|
|
||||||
for (locale, _, switcher) in &self.voice_packages {
|
|
||||||
if switcher.state() {
|
|
||||||
voices.push(locale.to_code().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.game.voices = voices;
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Page {
|
|
||||||
pub page: gtk::Box,
|
|
||||||
pub continue_button: gtk::Button,
|
|
||||||
pub advanced_button: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Page {
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/first_run/welcome.ui");
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
continue_button: get_object(&builder, "continue_button")?,
|
|
||||||
advanced_button: get_object(&builder, "advanced_button")?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
1105
src/ui/main.rs
1105
src/ui/main.rs
File diff suppressed because it is too large
Load diff
|
@ -1,49 +1,2 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
pub mod first_run;
|
|
||||||
pub mod main;
|
pub mod main;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod traits;
|
|
||||||
|
|
||||||
pub mod components;
|
|
||||||
|
|
||||||
pub use first_run::App as FirstRunApp;
|
|
||||||
pub use main::App as MainApp;
|
|
||||||
|
|
||||||
/// This function loads object from builder or panics if it doesn't exist
|
|
||||||
pub fn get_object<T: IsA<gtk::glib::Object>>(builder: >k::Builder, name: &str) -> anyhow::Result<T> {
|
|
||||||
match builder.object::<T>(name) {
|
|
||||||
Some(object) => Ok(object),
|
|
||||||
None => Err(anyhow::anyhow!("Failed to parse object '{name}'"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add action to widget
|
|
||||||
///
|
|
||||||
/// All the actions needs to be in some group. This function creates new group with the name of the action.
|
|
||||||
/// This means that to add action to some widget you need to speify `name.name` as its name
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let toast = libadwaita::Toast::new("Example toast");
|
|
||||||
///
|
|
||||||
/// toast.set_button_label(Some("Example button"));
|
|
||||||
/// toast.set_action_name(Some("example-button.example-button"));
|
|
||||||
///
|
|
||||||
/// add_action(&toast, "example-button", || {
|
|
||||||
/// println!("Hello, World!");
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
pub fn add_action<T: IsA<gtk::Widget>, F: Fn() + 'static>(obj: &T, name: &str, closure: F) {
|
|
||||||
let action_group = adw::gio::SimpleActionGroup::new();
|
|
||||||
let action = adw::gio::SimpleAction::new(name, None);
|
|
||||||
|
|
||||||
obj.insert_action_group(name, Some(&action_group));
|
|
||||||
|
|
||||||
action.connect_activate(move |_, _| {
|
|
||||||
closure();
|
|
||||||
});
|
|
||||||
|
|
||||||
action_group.add_action(&action);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,402 +1,212 @@
|
||||||
use gtk::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
use gtk::glib;
|
use anime_launcher_sdk::config;
|
||||||
use gtk::glib::clone;
|
use anime_launcher_sdk::config::prelude::*;
|
||||||
|
|
||||||
use crate::lib;
|
use crate::i18n::tr;
|
||||||
use crate::lib::config;
|
use crate::ui::main::is_ready;
|
||||||
use crate::lib::config::prelude::*;
|
|
||||||
|
|
||||||
use crate::ui::*;
|
lazy_static::lazy_static! {
|
||||||
|
static ref CONFIG: config::Config = config::get().expect("Failed to load config");
|
||||||
use super::gamescope::App as GamescopeApp;
|
|
||||||
|
|
||||||
/// This structure is used to describe widgets used in application
|
|
||||||
///
|
|
||||||
/// `AppWidgets::try_get` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets
|
|
||||||
///
|
|
||||||
/// This function does not implement events
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct AppWidgets {
|
|
||||||
pub page: adw::PreferencesPage,
|
|
||||||
|
|
||||||
pub sync_combo: adw::ComboRow,
|
|
||||||
pub wine_lang: adw::ComboRow,
|
|
||||||
pub borderless: gtk::Switch,
|
|
||||||
pub virtual_desktop_row: adw::ComboRow,
|
|
||||||
pub virtual_desktop: gtk::Switch,
|
|
||||||
|
|
||||||
pub hud_combo: adw::ComboRow,
|
|
||||||
pub fsr_combo: adw::ComboRow,
|
|
||||||
pub fsr_switcher: gtk::Switch,
|
|
||||||
|
|
||||||
pub gamemode_row: adw::ActionRow,
|
|
||||||
pub gamemode_switcher: gtk::Switch,
|
|
||||||
|
|
||||||
pub gamescope_row: adw::ActionRow,
|
|
||||||
pub gamescope_settings: gtk::Button,
|
|
||||||
pub gamescope_switcher: gtk::Switch,
|
|
||||||
|
|
||||||
pub gamescope_app: GamescopeApp,
|
|
||||||
|
|
||||||
pub fps_unlocker_combo: adw::ComboRow,
|
|
||||||
pub fps_unlocker_switcher: gtk::Switch,
|
|
||||||
pub fps_unlocker_power_saving_switcher: gtk::Switch,
|
|
||||||
pub fps_unlocker_monitor_num: gtk::SpinButton,
|
|
||||||
pub fps_unlocker_window_mode_combo: adw::ComboRow,
|
|
||||||
pub fps_unlocker_priority_combo: adw::ComboRow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppWidgets {
|
#[relm4::widget_template(pub)]
|
||||||
fn try_get(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
impl WidgetTemplate for Enhancements {
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences/enhancements.ui");
|
view! {
|
||||||
|
adw::PreferencesPage {
|
||||||
|
set_title: &tr("enhancements"),
|
||||||
|
set_icon_name: Some("applications-graphics-symbolic"),
|
||||||
|
|
||||||
let result = Self {
|
add = &adw::PreferencesGroup {
|
||||||
page: get_object(&builder, "page")?,
|
set_title: &tr("wine"),
|
||||||
|
|
||||||
sync_combo: get_object(&builder, "sync_combo")?,
|
adw::ComboRow {
|
||||||
wine_lang: get_object(&builder, "wine_lang")?,
|
set_title: &tr("synchronization"),
|
||||||
borderless: get_object(&builder, "borderless")?,
|
set_subtitle: &tr("wine-sync-description"),
|
||||||
virtual_desktop_row: get_object(&builder, "virtual_desktop_row")?,
|
|
||||||
virtual_desktop: get_object(&builder, "virtual_desktop")?,
|
|
||||||
|
|
||||||
hud_combo: get_object(&builder, "hud_combo")?,
|
#[wrap(Some)]
|
||||||
fsr_combo: get_object(&builder, "fsr_combo")?,
|
set_model = >k::StringList::new(&[
|
||||||
fsr_switcher: get_object(&builder, "fsr_switcher")?,
|
&tr("none"),
|
||||||
|
"ESync",
|
||||||
|
"FSync",
|
||||||
|
"Futex2"
|
||||||
|
]),
|
||||||
|
|
||||||
gamemode_row: get_object(&builder, "gamemode_row")?,
|
set_selected: CONFIG.game.wine.sync.into(),
|
||||||
gamemode_switcher: get_object(&builder, "gamemode_switcher")?,
|
|
||||||
|
|
||||||
gamescope_row: get_object(&builder, "gamescope_row")?,
|
connect_selected_notify => move |row| {
|
||||||
gamescope_settings: get_object(&builder, "gamescope_settings")?,
|
if is_ready() {
|
||||||
gamescope_switcher: get_object(&builder, "gamescope_switcher")?,
|
|
||||||
|
|
||||||
gamescope_app: GamescopeApp::new(window)?,
|
|
||||||
|
|
||||||
fps_unlocker_combo: get_object(&builder, "fps_unlocker_combo")?,
|
|
||||||
fps_unlocker_switcher: get_object(&builder, "fps_unlocker_switcher")?,
|
|
||||||
fps_unlocker_power_saving_switcher: get_object(&builder, "fps_unlocker_power_saving_switcher")?,
|
|
||||||
fps_unlocker_monitor_num: get_object(&builder, "fps_unlocker_monitor_num")?,
|
|
||||||
fps_unlocker_window_mode_combo: get_object(&builder, "fps_unlocker_window_mode_combo")?,
|
|
||||||
fps_unlocker_priority_combo: get_object(&builder, "fps_unlocker_priority_combo")?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set availale wine languages
|
|
||||||
result.wine_lang.set_model(Some(&WineLang::get_model()));
|
|
||||||
|
|
||||||
// Set availale virtual desktop resolutions
|
|
||||||
result.virtual_desktop_row.set_model(Some(&Resolution::get_model()));
|
|
||||||
|
|
||||||
// Set availale fps unlocker limits
|
|
||||||
result.fps_unlocker_combo.set_model(Some(&Fps::get_model()));
|
|
||||||
|
|
||||||
// Disable gamemode row if it's not available
|
|
||||||
if !lib::is_available("gamemoderun") {
|
|
||||||
result.gamemode_row.set_sensitive(false);
|
|
||||||
result.gamemode_row.set_tooltip_text(Some("Gamemode is not installed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable gamescope row if it's not available
|
|
||||||
if !lib::is_available("gamescope") {
|
|
||||||
result.gamescope_row.set_sensitive(false);
|
|
||||||
result.gamescope_row.set_tooltip_text(Some("Gamescope is not installed"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main application structure
|
|
||||||
///
|
|
||||||
/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets
|
|
||||||
///
|
|
||||||
/// `Rc<Cell<Values>>` means this:
|
|
||||||
/// - `Rc` addeds ability to reference the same value from various clones of the structure.
|
|
||||||
/// This will guarantee us that inner `Cell<Values>` is the same for all the `App::clone()` values
|
|
||||||
/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference.
|
|
||||||
///
|
|
||||||
/// So we have a shared reference to some value that can be changed without mutable reference.
|
|
||||||
/// That's what we need and what we use in `App::update` method
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct App {
|
|
||||||
widgets: AppWidgets
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
/// Create new application
|
|
||||||
pub fn new(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
|
||||||
let result = Self {
|
|
||||||
widgets: AppWidgets::try_get(window)?
|
|
||||||
}.init_events();
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add default events and values to the widgets
|
|
||||||
fn init_events(self) -> Self {
|
|
||||||
// Wine sync selection
|
|
||||||
self.widgets.sync_combo.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
if let Ok(mut config) = config::get() {
|
||||||
config.game.wine.sync = WineSync::try_from(row.selected()).unwrap();
|
config.game.wine.sync = WineSync::try_from(row.selected()).unwrap();
|
||||||
|
|
||||||
config::update(config);
|
config::update(config);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Wine language selection
|
|
||||||
self.widgets.wine_lang.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.wine.language = WineLang::list()[row.selected() as usize];
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Borderless switching
|
|
||||||
self.widgets.borderless.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.wine.borderless = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
// Virtual desktop resolution selection
|
adw::ComboRow {
|
||||||
self.widgets.virtual_desktop_row.connect_selected_notify(move |row| {
|
set_title: &tr("language"),
|
||||||
|
set_subtitle: &tr("wine-lang-description"),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_model = >k::StringList::new(&[
|
||||||
|
&tr("system"),
|
||||||
|
"English",
|
||||||
|
"Русский",
|
||||||
|
"Deutsch",
|
||||||
|
"Português",
|
||||||
|
"Polska",
|
||||||
|
"Français",
|
||||||
|
"Español",
|
||||||
|
"中国",
|
||||||
|
"日本語",
|
||||||
|
"한국어"
|
||||||
|
]),
|
||||||
|
|
||||||
|
set_selected: CONFIG.game.wine.language.into(),
|
||||||
|
|
||||||
|
connect_selected_notify => move |row| {
|
||||||
|
if is_ready() {
|
||||||
if let Ok(mut config) = config::get() {
|
if let Ok(mut config) = config::get() {
|
||||||
let resolutions = Resolution::list();
|
config.game.wine.language = WineLang::try_from(row.selected()).unwrap();
|
||||||
|
|
||||||
if row.selected() > 0 {
|
|
||||||
let (w, h) = resolutions[row.selected() as usize - 1].get_pair();
|
|
||||||
|
|
||||||
config.game.wine.virtual_desktop.width = w;
|
|
||||||
config.game.wine.virtual_desktop.height = h;
|
|
||||||
|
|
||||||
config::update(config);
|
config::update(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Virtual desktop switching
|
adw::ActionRow {
|
||||||
self.widgets.virtual_desktop.connect_state_notify(move |switch| {
|
set_title: &tr("borderless-window"),
|
||||||
|
|
||||||
|
add_suffix = >k::Switch {
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::ComboRow {
|
||||||
|
set_title: &tr("virtual-desktop"),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_model = >k::StringList::new(&[
|
||||||
|
&tr("custom"),
|
||||||
|
"960x540",
|
||||||
|
"1280x720",
|
||||||
|
"1920x1080",
|
||||||
|
"2560x1440",
|
||||||
|
"3840x2160"
|
||||||
|
]),
|
||||||
|
|
||||||
|
set_selected: CONFIG.game.wine.virtual_desktop.get_resolution().into(),
|
||||||
|
|
||||||
|
connect_selected_notify => move |row| {
|
||||||
|
if is_ready() {
|
||||||
|
if let Ok(mut config) = config::get() {
|
||||||
|
let (width, height) = Resolution::try_from(row.selected()).unwrap().get_pair();
|
||||||
|
|
||||||
|
config.game.wine.virtual_desktop.width = width;
|
||||||
|
config.game.wine.virtual_desktop.height = height;
|
||||||
|
|
||||||
|
config::update(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add_suffix = >k::Switch {
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
|
||||||
|
set_state: CONFIG.game.wine.virtual_desktop.enabled,
|
||||||
|
|
||||||
|
connect_state_notify => move |switch| {
|
||||||
|
if is_ready() {
|
||||||
if let Ok(mut config) = config::get() {
|
if let Ok(mut config) = config::get() {
|
||||||
config.game.wine.virtual_desktop.enabled = switch.state();
|
config.game.wine.virtual_desktop.enabled = switch.state();
|
||||||
|
|
||||||
config::update(config);
|
config::update(config);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// HUD selection
|
|
||||||
self.widgets.hud_combo.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.hud = HUD::try_from(row.selected()).unwrap();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FSR strength selection
|
|
||||||
//
|
|
||||||
// Ultra Quality = 5
|
|
||||||
// Quality = 4
|
|
||||||
// Balanced = 3
|
|
||||||
// Performance = 2
|
|
||||||
//
|
|
||||||
// Source: Bottles (https://github.com/bottlesdevs/Bottles/blob/22fa3573a13f4e9b9c429e4cdfe4ca29787a2832/src/ui/details-preferences.ui#L88)
|
|
||||||
self.widgets.fsr_combo.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fsr.strength = 5 - row.selected() as u64;
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FSR switching
|
|
||||||
self.widgets.fsr_switcher.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fsr.enabled = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gamemode switching
|
|
||||||
self.widgets.gamemode_switcher.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamemode = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gamescope settings app
|
|
||||||
self.widgets.gamescope_settings.connect_clicked(clone!(@weak self as this => move |_| {
|
|
||||||
this.widgets.gamescope_app.show();
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Gamescope swithing
|
|
||||||
self.widgets.gamescope_switcher.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.enabled = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FPS unlocker swithing
|
|
||||||
self.widgets.fps_unlocker_switcher.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fps_unlocker.enabled = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FPS unlocker -> fps limit combo
|
|
||||||
self.widgets.fps_unlocker_combo.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
if row.selected() > 0 {
|
|
||||||
config.game.enhancements.fps_unlocker.config.fps = Fps::list()[row.selected() as usize - 1].to_num();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FPS unlocker -> power saving swithing
|
|
||||||
self.widgets.fps_unlocker_power_saving_switcher.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fps_unlocker.config.power_saving = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FPS unlocker -> monitor number
|
|
||||||
self.widgets.fps_unlocker_monitor_num.connect_changed(move |button| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fps_unlocker.config.monitor = button.value() as u64;
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FPS unlocker -> window mode combo
|
|
||||||
self.widgets.fps_unlocker_window_mode_combo.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fps_unlocker.config.window_mode = row.selected() as u64;
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FPS unlocker -> priority combo
|
|
||||||
self.widgets.fps_unlocker_priority_combo.connect_selected_notify(move |row| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.fps_unlocker.config.priority = row.selected() as u64;
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title() -> String {
|
|
||||||
String::from("Enhancements")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_page(&self) -> adw::PreferencesPage {
|
|
||||||
self.widgets.page.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method is being called by the `PreferencesStack::update`
|
|
||||||
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
status_page.set_description(Some("Loading enhancements..."));
|
|
||||||
|
|
||||||
// Update Wine sync
|
|
||||||
self.widgets.sync_combo.set_selected(config.game.wine.sync.into());
|
|
||||||
|
|
||||||
// Update wine language
|
|
||||||
self.widgets.wine_lang.set_selected(config.game.wine.language.into());
|
|
||||||
|
|
||||||
// Update borderless
|
|
||||||
self.widgets.borderless.set_state(config.game.wine.borderless);
|
|
||||||
|
|
||||||
// Update virtual desktop
|
|
||||||
self.widgets.virtual_desktop.set_state(config.game.wine.virtual_desktop.enabled);
|
|
||||||
|
|
||||||
let resolution = Resolution::from_pair(
|
|
||||||
config.game.wine.virtual_desktop.width,
|
|
||||||
config.game.wine.virtual_desktop.height
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Resolution::Custom(_, _) = resolution {
|
|
||||||
self.widgets.virtual_desktop_row.set_selected(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
for (i, res) in Resolution::list().into_iter().enumerate() {
|
|
||||||
if res == resolution {
|
|
||||||
self.widgets.virtual_desktop_row.set_selected(i as u32 + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update HUD
|
|
||||||
self.widgets.hud_combo.set_selected(config.game.enhancements.hud.into());
|
|
||||||
|
|
||||||
// FSR strength selection
|
|
||||||
self.widgets.fsr_combo.set_selected(5 - config.game.enhancements.fsr.strength as u32);
|
|
||||||
|
|
||||||
// FSR switching
|
|
||||||
self.widgets.fsr_switcher.set_state(config.game.enhancements.fsr.enabled);
|
|
||||||
|
|
||||||
// Gamemode switching
|
|
||||||
self.widgets.gamemode_switcher.set_state(config.game.enhancements.gamemode);
|
|
||||||
|
|
||||||
// Switch gamescope option
|
|
||||||
self.widgets.gamescope_switcher.set_state(config.game.enhancements.gamescope.enabled);
|
|
||||||
|
|
||||||
// Switch FPS unlocker
|
|
||||||
self.widgets.fps_unlocker_switcher.set_state(config.game.enhancements.fps_unlocker.enabled);
|
|
||||||
|
|
||||||
// Select FPS limit
|
|
||||||
let fps = Fps::from_num(config.game.enhancements.fps_unlocker.config.fps);
|
|
||||||
|
|
||||||
if let Fps::Custom(_) = fps {
|
|
||||||
self.widgets.fps_unlocker_combo.set_selected(0);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
else {
|
add = &adw::PreferencesGroup {
|
||||||
for (i, value) in Fps::list().into_iter().enumerate() {
|
set_title: &tr("game"),
|
||||||
if value == fps {
|
|
||||||
self.widgets.fps_unlocker_combo.set_selected(i as u32 + 1);
|
adw::ComboRow {
|
||||||
|
set_title: &tr("hud"),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_model = >k::StringList::new(&[
|
||||||
|
&tr("none"),
|
||||||
|
"DXVK",
|
||||||
|
"MangoHud"
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::ComboRow {
|
||||||
|
set_title: &tr("fsr"),
|
||||||
|
set_subtitle: &tr("fsr-description"),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_model = >k::StringList::new(&[
|
||||||
|
&tr("ultra-quality"),
|
||||||
|
&tr("quality"),
|
||||||
|
&tr("balanced"),
|
||||||
|
&tr("performance")
|
||||||
|
]),
|
||||||
|
|
||||||
|
add_suffix = >k::Switch {
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::ActionRow {
|
||||||
|
set_title: &tr("gamemode"),
|
||||||
|
set_subtitle: &tr("gamemode-description"),
|
||||||
|
|
||||||
|
add_suffix = >k::Switch {
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add = &adw::PreferencesGroup {
|
||||||
|
set_title: &tr("fps-unlocker"),
|
||||||
|
|
||||||
|
adw::ComboRow {
|
||||||
|
set_title: &tr("enabled"),
|
||||||
|
set_subtitle: &tr("fps-unlocker-description"),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_model = >k::StringList::new(&[
|
||||||
|
&tr("custom"),
|
||||||
|
"90",
|
||||||
|
"120",
|
||||||
|
"144",
|
||||||
|
"165",
|
||||||
|
"180",
|
||||||
|
"200",
|
||||||
|
"240"
|
||||||
|
]),
|
||||||
|
|
||||||
|
add_suffix = >k::Switch {
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::ActionRow {
|
||||||
|
set_title: &tr("power-saving"),
|
||||||
|
set_subtitle: &tr("power-saving-description"),
|
||||||
|
|
||||||
|
add_suffix = >k::Switch {
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch FPS unlocker -> power saving
|
|
||||||
self.widgets.fps_unlocker_power_saving_switcher.set_state(config.game.enhancements.fps_unlocker.config.power_saving);
|
|
||||||
|
|
||||||
// Switch FPS unlocker -> monitor number
|
|
||||||
self.widgets.fps_unlocker_monitor_num.set_value(config.game.enhancements.fps_unlocker.config.monitor as f64);
|
|
||||||
|
|
||||||
// Switch FPS unlocker -> window mode
|
|
||||||
self.widgets.fps_unlocker_window_mode_combo.set_selected(config.game.enhancements.fps_unlocker.config.window_mode as u32);
|
|
||||||
|
|
||||||
// Switch FPS unlocker -> priority
|
|
||||||
self.widgets.fps_unlocker_priority_combo.set_selected(config.game.enhancements.fps_unlocker.config.priority as u32);
|
|
||||||
|
|
||||||
// Prepare gamescope settings app
|
|
||||||
self.widgets.gamescope_app.prepare(status_page)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for App {}
|
|
||||||
unsafe impl Sync for App {}
|
|
||||||
|
|
|
@ -1,243 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
use adw::prelude::*;
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
use gtk::glib::clone;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::cell::Cell;
|
|
||||||
|
|
||||||
use crate::ui::get_object;
|
|
||||||
use crate::lib::config;
|
|
||||||
|
|
||||||
/// This structure is used to describe widgets used in application
|
|
||||||
///
|
|
||||||
/// `AppWidgets::try_get` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets
|
|
||||||
///
|
|
||||||
/// This function does not implement events
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct AppWidgets {
|
|
||||||
pub page: adw::PreferencesPage,
|
|
||||||
|
|
||||||
pub command: gtk::Entry,
|
|
||||||
|
|
||||||
pub variables: adw::PreferencesGroup,
|
|
||||||
|
|
||||||
pub name: gtk::Entry,
|
|
||||||
pub value: gtk::Entry,
|
|
||||||
pub add: gtk::Button
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppWidgets {
|
|
||||||
fn try_get() -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences/environment.ui");
|
|
||||||
|
|
||||||
let result = Self {
|
|
||||||
page: get_object(&builder, "page")?,
|
|
||||||
|
|
||||||
command: get_object(&builder, "command")?,
|
|
||||||
|
|
||||||
variables: get_object(&builder, "variables")?,
|
|
||||||
|
|
||||||
name: get_object(&builder, "name")?,
|
|
||||||
value: get_object(&builder, "value")?,
|
|
||||||
add: get_object(&builder, "add")?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This enum is used to describe an action inside of this application
|
|
||||||
///
|
|
||||||
/// It may be helpful if you want to add the same event for several widgets, or call an action inside of another action
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Actions {
|
|
||||||
Add(Rc<(String, String)>),
|
|
||||||
Delete(Rc<String>)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This enum is used to store some of this application data
|
|
||||||
///
|
|
||||||
/// In this example we store a counter here to know what should we increment or decrement
|
|
||||||
///
|
|
||||||
/// This must implement `Default` trait
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Values {
|
|
||||||
rows: HashMap<String, adw::ActionRow>
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main application structure
|
|
||||||
///
|
|
||||||
/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets
|
|
||||||
///
|
|
||||||
/// `Rc<Cell<Values>>` means this:
|
|
||||||
/// - `Rc` addeds ability to reference the same value from various clones of the structure.
|
|
||||||
/// This will guarantee us that inner `Cell<Values>` is the same for all the `App::clone()` values
|
|
||||||
/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference.
|
|
||||||
///
|
|
||||||
/// So we have a shared reference to some value that can be changed without mutable reference.
|
|
||||||
/// That's what we need and what we use in `App::update` method
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct App {
|
|
||||||
widgets: AppWidgets,
|
|
||||||
values: Rc<Cell<Values>>,
|
|
||||||
actions: Rc<Cell<Option<glib::Sender<Actions>>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
/// Create new application
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let result = Self {
|
|
||||||
widgets: AppWidgets::try_get()?,
|
|
||||||
values: Default::default(),
|
|
||||||
actions: Default::default()
|
|
||||||
}.init_events().init_actions();
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add default events and values to the widgets
|
|
||||||
fn init_events(self) -> Self {
|
|
||||||
let this = self.clone();
|
|
||||||
|
|
||||||
self.widgets.add.connect_clicked(move |_| {
|
|
||||||
let name = this.widgets.name.text().to_string();
|
|
||||||
let value = this.widgets.value.text().to_string();
|
|
||||||
|
|
||||||
this.update(Actions::Add(Rc::new((name, value)))).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.widgets.command.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
let command = entry.text().to_string();
|
|
||||||
|
|
||||||
config.game.command = if command.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(command)
|
|
||||||
};
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add actions processors
|
|
||||||
///
|
|
||||||
/// Changes will happen in the main thread so you can call `update` method from separate thread
|
|
||||||
fn init_actions(self) -> Self {
|
|
||||||
let (sender, receiver) = glib::MainContext::channel::<Actions>(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
// I prefer to avoid using clone! here because it breaks my code autocompletion
|
|
||||||
let this = self.clone();
|
|
||||||
|
|
||||||
receiver.attach(None, move |action| {
|
|
||||||
let mut config = config::get().expect("Failed to load config");
|
|
||||||
let mut values = this.values.take();
|
|
||||||
|
|
||||||
// Some debug output
|
|
||||||
println!("[environment page] [update] action: {:?}", &action);
|
|
||||||
|
|
||||||
match action {
|
|
||||||
Actions::Add(strs) => {
|
|
||||||
let (name, value) = &*strs;
|
|
||||||
|
|
||||||
if !name.is_empty() && !value.is_empty() && !values.rows.contains_key(name) {
|
|
||||||
config.game.environment.insert(name.clone(), value.clone());
|
|
||||||
|
|
||||||
let row = adw::ActionRow::new();
|
|
||||||
|
|
||||||
row.set_title(name);
|
|
||||||
row.set_subtitle(value);
|
|
||||||
|
|
||||||
let button = gtk::Button::new();
|
|
||||||
|
|
||||||
button.set_icon_name("user-trash-symbolic");
|
|
||||||
button.set_valign(gtk::Align::Center);
|
|
||||||
button.add_css_class("flat");
|
|
||||||
|
|
||||||
button.connect_clicked(clone!(@weak this, @strong name => move |_| {
|
|
||||||
this.update(Actions::Delete(Rc::new(name.clone()))).unwrap();
|
|
||||||
}));
|
|
||||||
|
|
||||||
row.add_suffix(&button);
|
|
||||||
|
|
||||||
this.widgets.variables.add(&row);
|
|
||||||
|
|
||||||
values.rows.insert(name.clone(), row);
|
|
||||||
|
|
||||||
this.widgets.name.set_text("");
|
|
||||||
this.widgets.value.set_text("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::Delete(name) => {
|
|
||||||
let name = &*name;
|
|
||||||
|
|
||||||
if let Some(widget) = values.rows.get(name) {
|
|
||||||
this.widgets.variables.remove(widget);
|
|
||||||
}
|
|
||||||
|
|
||||||
values.rows.remove(name);
|
|
||||||
config.game.environment.remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
|
|
||||||
this.values.set(values);
|
|
||||||
|
|
||||||
glib::Continue(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.actions.set(Some(sender));
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update widgets state by calling some action
|
|
||||||
pub fn update(&self, action: Actions) -> Result<(), std::sync::mpsc::SendError<Actions>> {
|
|
||||||
let actions = self.actions.take();
|
|
||||||
|
|
||||||
let result = match &actions {
|
|
||||||
Some(sender) => Ok(sender.send(action)?),
|
|
||||||
None => Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
self.actions.set(actions);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title() -> String {
|
|
||||||
String::from("Environment")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_page(&self) -> adw::PreferencesPage {
|
|
||||||
self.widgets.page.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method is being called by the `PreferencesStack::update`
|
|
||||||
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
status_page.set_description(Some("Loading environment..."));
|
|
||||||
|
|
||||||
// Set game command
|
|
||||||
self.widgets.command.set_text(&config.game.command.unwrap_or_default());
|
|
||||||
|
|
||||||
// Add environment variables
|
|
||||||
for (name, value) in config.game.environment {
|
|
||||||
self.update(Actions::Add(Rc::new((name, value)))).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for App {}
|
|
||||||
unsafe impl Sync for App {}
|
|
|
@ -1,260 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
use crate::lib::config;
|
|
||||||
use crate::lib::config::prelude::*;
|
|
||||||
|
|
||||||
use crate::ui::*;
|
|
||||||
|
|
||||||
/// This structure is used to describe widgets used in application
|
|
||||||
///
|
|
||||||
/// `AppWidgets::try_get` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets
|
|
||||||
///
|
|
||||||
/// This function does not implement events
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct AppWidgets {
|
|
||||||
pub window: adw::PreferencesWindow,
|
|
||||||
|
|
||||||
pub game_width: adw::EntryRow,
|
|
||||||
pub game_height: adw::EntryRow,
|
|
||||||
|
|
||||||
pub gamescope_width: adw::EntryRow,
|
|
||||||
pub gamescope_height: adw::EntryRow,
|
|
||||||
|
|
||||||
pub framerate_limit: adw::EntryRow,
|
|
||||||
pub framerate_unfocused_limit: adw::EntryRow,
|
|
||||||
pub integer_scaling: gtk::Switch,
|
|
||||||
pub fsr: gtk::Switch,
|
|
||||||
pub nis: gtk::Switch,
|
|
||||||
|
|
||||||
pub borderless: gtk::ToggleButton,
|
|
||||||
pub fullscreen: gtk::ToggleButton
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppWidgets {
|
|
||||||
fn try_get(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences/gamescope.ui");
|
|
||||||
|
|
||||||
let result = Self {
|
|
||||||
window: get_object(&builder, "window")?,
|
|
||||||
|
|
||||||
game_width: get_object(&builder, "game_width")?,
|
|
||||||
game_height: get_object(&builder, "game_height")?,
|
|
||||||
|
|
||||||
gamescope_width: get_object(&builder, "gamescope_width")?,
|
|
||||||
gamescope_height: get_object(&builder, "gamescope_height")?,
|
|
||||||
|
|
||||||
framerate_limit: get_object(&builder, "framerate_limit")?,
|
|
||||||
framerate_unfocused_limit: get_object(&builder, "framerate_unfocused_limit")?,
|
|
||||||
integer_scaling: get_object(&builder, "integer_scaling")?,
|
|
||||||
fsr: get_object(&builder, "fsr")?,
|
|
||||||
nis: get_object(&builder, "nis")?,
|
|
||||||
|
|
||||||
borderless: get_object(&builder, "borderless")?,
|
|
||||||
fullscreen: get_object(&builder, "fullscreen")?
|
|
||||||
};
|
|
||||||
|
|
||||||
result.window.set_transient_for(Some(window));
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main application structure
|
|
||||||
///
|
|
||||||
/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets
|
|
||||||
///
|
|
||||||
/// `Rc<Cell<Values>>` means this:
|
|
||||||
/// - `Rc` addeds ability to reference the same value from various clones of the structure.
|
|
||||||
/// This will guarantee us that inner `Cell<Values>` is the same for all the `App::clone()` values
|
|
||||||
/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference.
|
|
||||||
///
|
|
||||||
/// So we have a shared reference to some value that can be changed without mutable reference.
|
|
||||||
/// That's what we need and what we use in `App::update` method
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct App {
|
|
||||||
widgets: AppWidgets
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
/// Create new application
|
|
||||||
pub fn new(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
|
||||||
let result = Self {
|
|
||||||
widgets: AppWidgets::try_get(window)?
|
|
||||||
}.init_events();
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add default events and values to the widgets
|
|
||||||
fn init_events(self) -> Self {
|
|
||||||
// Game width
|
|
||||||
self.widgets.game_width.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.game.width = entry.text().parse().unwrap_or(0);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Game height
|
|
||||||
self.widgets.game_height.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.game.height = entry.text().parse().unwrap_or(0);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gamescope width
|
|
||||||
self.widgets.gamescope_width.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.gamescope.width = entry.text().parse().unwrap_or(0);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gamescope height
|
|
||||||
self.widgets.gamescope_height.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.gamescope.height = entry.text().parse().unwrap_or(0);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Framerate focused
|
|
||||||
self.widgets.framerate_limit.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.framerate.focused = entry.text().parse().unwrap_or(0);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Framerate unfocused
|
|
||||||
self.widgets.framerate_unfocused_limit.connect_changed(move |entry| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.framerate.unfocused = entry.text().parse().unwrap_or(0);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use integer scaling
|
|
||||||
self.widgets.integer_scaling.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.integer_scaling = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use FSR
|
|
||||||
self.widgets.fsr.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.fsr = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use NIS (Nvidia Image Scaling)
|
|
||||||
self.widgets.nis.connect_state_notify(move |switch| {
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.nis = switch.state();
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Window type
|
|
||||||
|
|
||||||
let borderless = self.widgets.borderless.clone();
|
|
||||||
let fullscreen = self.widgets.fullscreen.clone();
|
|
||||||
|
|
||||||
// Window type (Borderless)
|
|
||||||
self.widgets.borderless.connect_clicked(move |button| {
|
|
||||||
if !button.is_active() {
|
|
||||||
button.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
fullscreen.set_active(false);
|
|
||||||
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.window_type = if button.is_active() {
|
|
||||||
WindowType::Borderless
|
|
||||||
} else {
|
|
||||||
WindowType::Fullscreen
|
|
||||||
};
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Window type (Fullscreen)
|
|
||||||
self.widgets.fullscreen.connect_clicked(move |button| {
|
|
||||||
if !button.is_active() {
|
|
||||||
button.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
borderless.set_active(false);
|
|
||||||
|
|
||||||
if let Ok(mut config) = config::get() {
|
|
||||||
config.game.enhancements.gamescope.window_type = if button.is_active() {
|
|
||||||
WindowType::Fullscreen
|
|
||||||
} else {
|
|
||||||
WindowType::Borderless
|
|
||||||
};
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method is being called by the `EnhancementsPage::prepare`
|
|
||||||
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
status_page.set_description(Some("Loading gamescope..."));
|
|
||||||
|
|
||||||
fn set_text(widget: &adw::EntryRow, value: u64) {
|
|
||||||
widget.set_text(&if value == 0 { String::new() } else { value.to_string() });
|
|
||||||
}
|
|
||||||
|
|
||||||
set_text(&self.widgets.game_width, config.game.enhancements.gamescope.game.width);
|
|
||||||
set_text(&self.widgets.game_height, config.game.enhancements.gamescope.game.height);
|
|
||||||
|
|
||||||
set_text(&self.widgets.gamescope_width, config.game.enhancements.gamescope.gamescope.width);
|
|
||||||
set_text(&self.widgets.gamescope_height, config.game.enhancements.gamescope.gamescope.height);
|
|
||||||
|
|
||||||
set_text(&self.widgets.framerate_limit, config.game.enhancements.gamescope.framerate.focused);
|
|
||||||
set_text(&self.widgets.framerate_unfocused_limit, config.game.enhancements.gamescope.framerate.unfocused);
|
|
||||||
|
|
||||||
self.widgets.integer_scaling.set_state(config.game.enhancements.gamescope.integer_scaling);
|
|
||||||
self.widgets.fsr.set_state(config.game.enhancements.gamescope.fsr);
|
|
||||||
self.widgets.nis.set_state(config.game.enhancements.gamescope.nis);
|
|
||||||
|
|
||||||
match config.game.enhancements.gamescope.window_type {
|
|
||||||
WindowType::Borderless => self.widgets.borderless.set_active(true),
|
|
||||||
WindowType::Fullscreen => self.widgets.fullscreen.set_active(true)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show(&self) {
|
|
||||||
self.widgets.window.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for App {}
|
|
||||||
unsafe impl Sync for App {}
|
|
|
@ -1,716 +1,134 @@
|
||||||
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
use gtk::glib;
|
use anime_launcher_sdk::config;
|
||||||
use gtk::glib::clone;
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
use crate::i18n::tr;
|
||||||
use std::cell::Cell;
|
use crate::ui::main::is_ready;
|
||||||
|
|
||||||
use anime_game_core::prelude::*;
|
lazy_static::lazy_static! {
|
||||||
use anime_game_core::genshin::prelude::*;
|
static ref CONFIG: config::Config = config::get().expect("Failed to load config");
|
||||||
|
|
||||||
use crate::lib::consts;
|
|
||||||
use crate::lib::config;
|
|
||||||
use crate::lib::dxvk;
|
|
||||||
use crate::lib::wine;
|
|
||||||
use crate::lib::launcher::states::LauncherState;
|
|
||||||
|
|
||||||
use crate::ui::*;
|
|
||||||
use crate::ui::traits::prelude::*;
|
|
||||||
use crate::ui::components::voiceover_row::VoiceoverRow;
|
|
||||||
use crate::ui::components::wine_group::WineGroup;
|
|
||||||
use crate::ui::components::dxvk_group::DxvkGroup;
|
|
||||||
|
|
||||||
/// This structure is used to describe widgets used in application
|
|
||||||
///
|
|
||||||
/// `AppWidgets::try_get` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets
|
|
||||||
///
|
|
||||||
/// This function does not implement events
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct AppWidgets {
|
|
||||||
pub page: adw::PreferencesPage,
|
|
||||||
|
|
||||||
pub voiceovers_row: adw::ExpanderRow,
|
|
||||||
pub voieover_components: Rc<Vec<VoiceoverRow>>,
|
|
||||||
|
|
||||||
pub repair_game: gtk::Button,
|
|
||||||
|
|
||||||
pub game_version: gtk::Label,
|
|
||||||
pub patch_version: gtk::Label,
|
|
||||||
|
|
||||||
pub wine_selected: adw::ComboRow,
|
|
||||||
|
|
||||||
pub wine_groups: adw::PreferencesGroup,
|
|
||||||
pub wine_recommended_only: gtk::Switch,
|
|
||||||
|
|
||||||
pub wine_components: Rc<Vec<WineGroup>>,
|
|
||||||
|
|
||||||
pub dxvk_selected: adw::ComboRow,
|
|
||||||
|
|
||||||
pub dxvk_groups: adw::PreferencesGroup,
|
|
||||||
pub dxvk_recommended_only: gtk::Switch,
|
|
||||||
|
|
||||||
pub dxvk_components: Rc<Vec<DxvkGroup>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppWidgets {
|
#[relm4::widget_template(pub)]
|
||||||
pub fn try_get() -> anyhow::Result<Self> {
|
impl WidgetTemplate for General {
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences/general.ui");
|
view! {
|
||||||
|
adw::PreferencesPage {
|
||||||
let mut result = Self {
|
set_title: &tr("general"),
|
||||||
page: get_object(&builder, "page")?,
|
set_icon_name: Some("applications-system-symbolic"),
|
||||||
|
|
||||||
voiceovers_row: get_object(&builder, "voiceovers_row")?,
|
add = &adw::PreferencesGroup {
|
||||||
voieover_components: Default::default(),
|
set_title: &tr("general"),
|
||||||
|
|
||||||
repair_game: get_object(&builder, "repair_game")?,
|
adw::ComboRow {
|
||||||
|
set_title: &tr("launcher-language"),
|
||||||
game_version: get_object(&builder, "game_version")?,
|
|
||||||
patch_version: get_object(&builder, "patch_version")?,
|
// TODO: maybe simplify it by some way? e.g. specify such stuff in i18n mod
|
||||||
|
|
||||||
wine_selected: get_object(&builder, "wine_selected")?,
|
#[wrap(Some)]
|
||||||
|
set_model = >k::StringList::new(&[
|
||||||
wine_groups: get_object(&builder, "wine_groups")?,
|
"English",
|
||||||
wine_recommended_only: get_object(&builder, "wine_recommended_only")?,
|
"Русский"
|
||||||
|
]),
|
||||||
wine_components: Default::default(),
|
|
||||||
|
set_selected: match CONFIG.launcher.language.as_str() {
|
||||||
dxvk_selected: get_object(&builder, "dxvk_selected")?,
|
"en-us" => 0,
|
||||||
|
"ru-ru" => 1,
|
||||||
dxvk_groups: get_object(&builder, "dxvk_groups")?,
|
_ => 0
|
||||||
dxvk_recommended_only: get_object(&builder, "dxvk_recommended_only")?,
|
},
|
||||||
|
|
||||||
dxvk_components: Default::default()
|
connect_selected_notify => move |row| {
|
||||||
};
|
if is_ready() {
|
||||||
|
if let Ok(mut config) = config::get() {
|
||||||
let config = config::get()?;
|
config.launcher.language = String::from(*[
|
||||||
|
"en-us",
|
||||||
// Update voiceovers list
|
"ru-ru"
|
||||||
let voice_packages = VoicePackage::list_latest()?;
|
].get(row.selected() as usize).unwrap_or(&"en-us"));
|
||||||
|
|
||||||
let mut components = Vec::new();
|
|
||||||
|
|
||||||
for package in voice_packages {
|
|
||||||
let row = VoiceoverRow::new(package);
|
|
||||||
|
|
||||||
result.voiceovers_row.add_row(&row.row);
|
|
||||||
|
|
||||||
components.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.voieover_components = Rc::new(components);
|
|
||||||
|
|
||||||
// Update wine versions lists
|
|
||||||
let mut components = Vec::new();
|
|
||||||
|
|
||||||
for group in wine::List::get() {
|
|
||||||
let group = WineGroup::new(group);
|
|
||||||
|
|
||||||
group.update_states(&config.game.wine.builds);
|
|
||||||
|
|
||||||
result.wine_groups.add(&group.expander_row);
|
|
||||||
|
|
||||||
components.push(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.wine_components = Rc::new(components);
|
|
||||||
|
|
||||||
// Update DXVK list
|
|
||||||
let mut components = Vec::new();
|
|
||||||
|
|
||||||
for group in dxvk::List::get() {
|
|
||||||
let group = DxvkGroup::new(group);
|
|
||||||
|
|
||||||
group.update_states(&config.game.dxvk.builds);
|
|
||||||
|
|
||||||
result.dxvk_groups.add(&group.expander_row);
|
|
||||||
|
|
||||||
components.push(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.dxvk_components = Rc::new(components);
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This enum is used to describe an action inside of this application
|
|
||||||
///
|
|
||||||
/// It may be helpful if you want to add the same event for several widgets, or call an action inside of another action
|
|
||||||
#[derive(Debug, Clone, glib::Downgrade)]
|
|
||||||
pub enum Actions {
|
|
||||||
RepairGame,
|
|
||||||
VoiceoverPerformAction(Rc<usize>),
|
|
||||||
DxvkPerformAction(Rc<(usize, usize)>),
|
|
||||||
WinePerformAction(Rc<(usize, usize)>),
|
|
||||||
UpdateDxvkComboRow,
|
|
||||||
SelectDxvkVersion(Rc<usize>),
|
|
||||||
UpdateWineComboRow,
|
|
||||||
SelectWineVersion(Rc<usize>),
|
|
||||||
Toast(Rc<(String, String)>)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actions {
|
|
||||||
#[allow(clippy::expect_fun_call, clippy::wrong_self_convention)]
|
|
||||||
pub fn into_fn<T: gtk::glib::IsA<gtk::Widget>>(&self, app: &App) -> Box<dyn Fn(&T)> {
|
|
||||||
Box::new(clone!(@strong self as action, @weak app => move |_| {
|
|
||||||
app.update(action.clone()).expect(&format!("Failed to execute action {:?}", &action));
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This enum is used to store some of this application data
|
|
||||||
///
|
|
||||||
/// In this example we store a counter here to know what should we increment or decrement
|
|
||||||
///
|
|
||||||
/// This must implement `Default` trait
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Values {
|
|
||||||
downloaded_wine_versions: Option<Vec<wine::Version>>,
|
|
||||||
downloaded_dxvk_versions: Option<Vec<dxvk::Version>>
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main application structure
|
|
||||||
///
|
|
||||||
/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets
|
|
||||||
///
|
|
||||||
/// `Rc<Cell<Values>>` means this:
|
|
||||||
/// - `Rc` addeds ability to reference the same value from various clones of the structure.
|
|
||||||
/// This will guarantee us that inner `Cell<Values>` is the same for all the `App::clone()` values
|
|
||||||
/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference.
|
|
||||||
///
|
|
||||||
/// So we have a shared reference to some value that can be changed without mutable reference.
|
|
||||||
/// That's what we need and what we use in `App::update` method
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct App {
|
|
||||||
app: Rc<Cell<Option<super::MainApp>>>,
|
|
||||||
widgets: AppWidgets,
|
|
||||||
values: Rc<Cell<Values>>,
|
|
||||||
actions: Rc<Cell<Option<glib::Sender<Actions>>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
/// Create new application
|
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
|
||||||
let result = Self {
|
|
||||||
app: Default::default(),
|
|
||||||
widgets: AppWidgets::try_get()?,
|
|
||||||
values: Default::default(),
|
|
||||||
actions: Default::default()
|
|
||||||
}.init_events().init_actions();
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_app(&mut self, app: super::MainApp) {
|
|
||||||
self.app.set(Some(app));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add default events and values to the widgets
|
|
||||||
fn init_events(self) -> Self {
|
|
||||||
self.widgets.repair_game.connect_clicked(Actions::RepairGame.into_fn(&self));
|
|
||||||
|
|
||||||
// Voiceover download/delete button event
|
|
||||||
for (i, row) in (*self.widgets.voieover_components).iter().enumerate() {
|
|
||||||
row.button.connect_clicked(clone!(@weak self as this => move |_| {
|
|
||||||
this.update(Actions::VoiceoverPerformAction(Rc::new(i))).unwrap();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selecting wine version event
|
|
||||||
self.widgets.wine_selected.connect_selected_notify(clone!(@weak self as this => move |combo_row| {
|
|
||||||
if let Some(model) = combo_row.model() {
|
|
||||||
if model.n_items() > 0 {
|
|
||||||
this.update(Actions::SelectWineVersion(Rc::new(combo_row.selected() as usize))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Selecting dxvk version event
|
|
||||||
self.widgets.dxvk_selected.connect_selected_notify(clone!(@weak self as this => move |combo_row| {
|
|
||||||
if let Some(model) = combo_row.model() {
|
|
||||||
if model.n_items() > 0 {
|
|
||||||
this.update(Actions::SelectDxvkVersion(Rc::new(combo_row.selected() as usize))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Set wine recommended only switcher event
|
|
||||||
self.widgets.wine_recommended_only.connect_state_notify(clone!(@weak self as this => move |switcher| {
|
|
||||||
for group in &*this.widgets.wine_components {
|
|
||||||
for component in &group.version_components {
|
|
||||||
component.row.set_visible(if switcher.state() {
|
|
||||||
component.version.recommended
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Wine install/remove buttons
|
|
||||||
let components = &*self.widgets.wine_components;
|
|
||||||
|
|
||||||
for (i, group) in components.iter().enumerate() {
|
|
||||||
for (j, component) in group.version_components.iter().enumerate() {
|
|
||||||
component.button.connect_clicked(Actions::WinePerformAction(Rc::new((i, j))).into_fn(&self));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set DXVK recommended only switcher event
|
|
||||||
self.widgets.dxvk_recommended_only.connect_state_notify(clone!(@weak self as this => move |switcher| {
|
|
||||||
for group in &*this.widgets.dxvk_components {
|
|
||||||
for component in &group.version_components {
|
|
||||||
component.row.set_visible(if switcher.state() {
|
|
||||||
component.version.recommended
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// DXVK install/remove/apply buttons
|
|
||||||
let components = &*self.widgets.dxvk_components;
|
|
||||||
|
|
||||||
for (i, group) in components.iter().enumerate() {
|
|
||||||
for (j, component) in group.version_components.iter().enumerate() {
|
|
||||||
component.button.connect_clicked(Actions::DxvkPerformAction(Rc::new((i, j))).into_fn(&self));
|
|
||||||
|
|
||||||
component.apply_button.connect_clicked(clone!(@strong component, @weak self as this => move |_| {
|
|
||||||
std::thread::spawn(clone!(@strong component, @strong this => move || {
|
|
||||||
let config = config::get().expect("Failed to load config");
|
|
||||||
|
|
||||||
match component.apply(&config.game.dxvk.builds, &config.game.wine.prefix) {
|
|
||||||
Ok(output) => println!("{}", String::from_utf8_lossy(&output.stdout)),
|
|
||||||
Err(err) => {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to apply DXVK"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add actions processors
|
|
||||||
///
|
|
||||||
/// Changes will happen in the main thread so you can call `update` method from separate thread
|
|
||||||
fn init_actions(self) -> Self {
|
|
||||||
let (sender, receiver) = glib::MainContext::channel::<Actions>(glib::PRIORITY_DEFAULT);
|
|
||||||
|
|
||||||
// I prefer to avoid using clone! here because it breaks my code autocompletion
|
|
||||||
let this = self.clone();
|
|
||||||
|
|
||||||
receiver.attach(None, move |action| {
|
|
||||||
let mut config = config::get().expect("Failed to load config");
|
|
||||||
|
|
||||||
// Some debug output
|
|
||||||
println!("[general page] [update] action: {:?}", &action);
|
|
||||||
|
|
||||||
match action {
|
|
||||||
Actions::RepairGame => {
|
|
||||||
let option = (*this.app).take();
|
|
||||||
this.app.set(option.clone());
|
|
||||||
|
|
||||||
let app = option.unwrap();
|
|
||||||
|
|
||||||
app.update(super::main::Actions::PreferencesGoBack).unwrap();
|
|
||||||
app.update(super::main::Actions::RepairGame).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::VoiceoverPerformAction(i) => {
|
|
||||||
let component = this.widgets.voieover_components[*i].clone();
|
|
||||||
|
|
||||||
if component.is_downloaded(&config.game.path) {
|
|
||||||
component.button.set_sensitive(false);
|
|
||||||
|
|
||||||
let this = this.clone();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
if let Err(err) = component.package.delete_in(&config.game.path) {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to delete voiceover"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
component.button.set_sensitive(true);
|
|
||||||
|
|
||||||
component.update_state(&config.game.path);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
let option = (*this.app).take();
|
|
||||||
this.app.set(option.clone());
|
|
||||||
|
|
||||||
let app = option.unwrap();
|
|
||||||
|
|
||||||
// Add voiceover to config
|
|
||||||
config.game.voices.push(component.package.locale().to_code().to_string());
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
|
|
||||||
// Return back, update state and press "download" button if needed
|
|
||||||
app.update(super::main::Actions::PreferencesGoBack).unwrap();
|
|
||||||
app.update_state().then(move |state| {
|
|
||||||
if let Ok(LauncherState::VoiceNotInstalled(_)) = state {
|
|
||||||
app.update(super::main::Actions::PerformButtonEvent).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::DxvkPerformAction(version) => {
|
|
||||||
let component = this.widgets
|
|
||||||
.dxvk_components[version.0]
|
|
||||||
.version_components[version.1].clone();
|
|
||||||
|
|
||||||
if component.is_downloaded(&config.game.dxvk.builds) {
|
|
||||||
if let Err(err) = component.delete(&config.game.dxvk.builds) {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to delete DXVK"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
component.update_state(&config.game.dxvk.builds);
|
|
||||||
|
|
||||||
this.update(Actions::UpdateDxvkComboRow).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
else if let Ok(awaiter) = component.download(&config.game.dxvk.builds) {
|
|
||||||
awaiter.then(clone!(@strong this => move |_| {
|
|
||||||
match component.apply(&config.game.dxvk.builds, &config.game.wine.prefix) {
|
|
||||||
Ok(output) => println!("{}", String::from_utf8_lossy(&output.stdout)),
|
|
||||||
Err(err) => {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to apply DXVK"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component.update_state(&config.game.dxvk.builds);
|
|
||||||
|
|
||||||
this.update(Actions::UpdateDxvkComboRow).unwrap();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::WinePerformAction(version) => {
|
|
||||||
let component = this.widgets
|
|
||||||
.wine_components[version.0]
|
|
||||||
.version_components[version.1].clone();
|
|
||||||
|
|
||||||
if component.is_downloaded(&config.game.wine.builds) {
|
|
||||||
if let Err(err) = component.delete(&config.game.wine.builds) {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to delete wine"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
component.update_state(&config.game.wine.builds);
|
|
||||||
|
|
||||||
this.update(Actions::UpdateWineComboRow).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
else if let Ok(awaiter) = component.download(&config.game.wine.builds) {
|
|
||||||
awaiter.then(clone!(@strong this => move |_| {
|
|
||||||
component.update_state(&config.game.wine.builds);
|
|
||||||
|
|
||||||
this.update(Actions::UpdateWineComboRow).unwrap();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::UpdateDxvkComboRow => {
|
|
||||||
let model = gtk::StringList::new(&[]);
|
|
||||||
|
|
||||||
let list = dxvk::List::list_downloaded(&config.game.dxvk.builds)
|
|
||||||
.expect("Failed to list downloaded DXVK versions");
|
|
||||||
|
|
||||||
let mut raw_list = Vec::new();
|
|
||||||
let mut selected = 0;
|
|
||||||
|
|
||||||
let curr = match config.try_get_selected_dxvk_info() {
|
|
||||||
Ok(Some(curr)) => Some(curr.name),
|
|
||||||
_ => None
|
|
||||||
};
|
|
||||||
|
|
||||||
for version in list {
|
|
||||||
model.append(&version.name);
|
|
||||||
|
|
||||||
if let Some(curr) = &curr {
|
|
||||||
if &version.name == curr {
|
|
||||||
selected = raw_list.len() as u32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
raw_list.push(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut values = this.values.take();
|
|
||||||
|
|
||||||
values.downloaded_dxvk_versions = Some(raw_list);
|
|
||||||
|
|
||||||
this.values.set(values);
|
|
||||||
|
|
||||||
// This will prevent SelectDxvkVersion action to be invoked
|
|
||||||
let guard = this.widgets.dxvk_selected.freeze_notify();
|
|
||||||
|
|
||||||
// We need to return app values before we call these methods
|
|
||||||
// because they'll invoke SelectWineVersion action so access
|
|
||||||
// downloaded_wine_versions value
|
|
||||||
this.widgets.dxvk_selected.set_model(Some(&model));
|
|
||||||
this.widgets.dxvk_selected.set_selected(selected);
|
|
||||||
|
|
||||||
drop(guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::SelectDxvkVersion(i) => {
|
|
||||||
let values = this.values.take();
|
|
||||||
|
|
||||||
if let Some(dxvk_versions) = &values.downloaded_dxvk_versions {
|
|
||||||
let version = dxvk_versions[*i].clone();
|
|
||||||
let mut apply = true;
|
|
||||||
|
|
||||||
if let Ok(Some(curr)) = config.try_get_selected_dxvk_info() {
|
|
||||||
if version == curr {
|
|
||||||
apply = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if apply {
|
|
||||||
this.widgets.dxvk_selected.set_sensitive(false);
|
|
||||||
|
|
||||||
std::thread::spawn(clone!(@strong config, @strong this => move || {
|
|
||||||
match version.apply(&config.game.dxvk.builds, &config.game.wine.prefix) {
|
|
||||||
Ok(output) => println!("{}", String::from_utf8_lossy(&output.stdout)),
|
|
||||||
Err(err) => {
|
|
||||||
this.update(Actions::Toast(Rc::new((
|
|
||||||
String::from("Failed to apply DXVK"), err.to_string()
|
|
||||||
)))).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.widgets.dxvk_selected.set_sensitive(true);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.values.set(values);
|
|
||||||
|
|
||||||
config::update(config);
|
config::update(config);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
Actions::UpdateWineComboRow => {
|
adw::ExpanderRow {
|
||||||
let model = gtk::StringList::new(&["System"]);
|
set_title: &tr("game-voiceovers"),
|
||||||
|
|
||||||
let list = wine::List::list_downloaded(config.game.wine.builds)
|
add_row = &adw::ActionRow {
|
||||||
.expect("Failed to list downloaded wine versions");
|
set_title: &tr("english"),
|
||||||
|
|
||||||
let mut selected = 0;
|
add_suffix = >k::Button {
|
||||||
|
add_css_class: "flat",
|
||||||
|
set_icon_name: "user-trash-symbolic",
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
for (i, version) in list.iter().enumerate() {
|
add_row = &adw::ActionRow {
|
||||||
model.append(version.title.as_str());
|
set_title: &tr("japanese"),
|
||||||
|
|
||||||
if let Some(curr) = &config.game.wine.selected {
|
add_suffix = >k::Button {
|
||||||
if &version.name == curr {
|
add_css_class: "flat",
|
||||||
selected = i as u32 + 1;
|
set_icon_name: "user-trash-symbolic",
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add_row = &adw::ActionRow {
|
||||||
|
set_title: &tr("korean"),
|
||||||
|
|
||||||
|
add_suffix = >k::Button {
|
||||||
|
add_css_class: "flat",
|
||||||
|
set_icon_name: "user-trash-symbolic",
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add_row = &adw::ActionRow {
|
||||||
|
set_title: &tr("chinese"),
|
||||||
|
|
||||||
|
add_suffix = >k::Button {
|
||||||
|
add_css_class: "flat",
|
||||||
|
set_icon_name: "user-trash-symbolic",
|
||||||
|
set_valign: gtk::Align::Center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_spacing: 8,
|
||||||
|
set_margin_top: 16,
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: &tr("repair-game")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add = &adw::PreferencesGroup {
|
||||||
|
set_title: &tr("status"),
|
||||||
|
|
||||||
|
adw::ActionRow {
|
||||||
|
set_title: &tr("game-version"),
|
||||||
|
|
||||||
|
add_suffix = >k::Label {
|
||||||
|
set_text: "3.3.0",
|
||||||
|
add_css_class: "success"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::ActionRow {
|
||||||
|
set_title: &tr("patch-version"),
|
||||||
|
|
||||||
|
add_suffix = >k::Label {
|
||||||
|
set_text: "3.3.0",
|
||||||
|
add_css_class: "success"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut values = this.values.take();
|
|
||||||
|
|
||||||
values.downloaded_wine_versions = Some(list);
|
|
||||||
|
|
||||||
this.values.set(values);
|
|
||||||
|
|
||||||
// This will prevent SelectWineVersion action to be invoked
|
|
||||||
let guard = this.widgets.wine_selected.freeze_notify();
|
|
||||||
|
|
||||||
// We need to return app values before we call these methods
|
|
||||||
// because they'll invoke SelectWineVersion action so access
|
|
||||||
// downloaded_wine_versions value
|
|
||||||
this.widgets.wine_selected.set_model(Some(&model));
|
|
||||||
this.widgets.wine_selected.set_selected(selected);
|
|
||||||
|
|
||||||
drop(guard);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::SelectWineVersion(i) => {
|
|
||||||
let values = this.values.take();
|
|
||||||
|
|
||||||
if let Some(wine_versions) = &values.downloaded_wine_versions {
|
|
||||||
match *i {
|
|
||||||
0 => config.game.wine.selected = None,
|
|
||||||
i => config.game.wine.selected = Some(wine_versions[i - 1].name.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.values.set(values);
|
|
||||||
|
|
||||||
config::update(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions::Toast(toast) => {
|
|
||||||
let (msg, err) = (toast.0.clone(), toast.1.to_string());
|
|
||||||
|
|
||||||
this.toast(msg, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::Continue(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.actions.set(Some(sender));
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update widgets state by calling some action
|
|
||||||
pub fn update(&self, action: Actions) -> Result<(), std::sync::mpsc::SendError<Actions>> {
|
|
||||||
let actions = self.actions.take();
|
|
||||||
|
|
||||||
let result = match &actions {
|
|
||||||
Some(sender) => Ok(sender.send(action)?),
|
|
||||||
None => Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
self.actions.set(actions);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title() -> String {
|
|
||||||
String::from("General")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_page(&self) -> adw::PreferencesPage {
|
|
||||||
self.widgets.page.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This method is being called by the `PreferencesStack::update`
|
|
||||||
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
|
|
||||||
let config = config::get()?;
|
|
||||||
let game = Game::new(&config.game.path);
|
|
||||||
|
|
||||||
// Update voiceovers states
|
|
||||||
status_page.set_description(Some("Updating voiceovers info..."));
|
|
||||||
|
|
||||||
for package in &*self.widgets.voieover_components {
|
|
||||||
package.update_state(&config.game.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update game version
|
|
||||||
status_page.set_description(Some("Updating game info..."));
|
|
||||||
|
|
||||||
self.widgets.game_version.set_tooltip_text(None);
|
|
||||||
self.widgets.patch_version.set_tooltip_text(None);
|
|
||||||
|
|
||||||
match game.try_get_diff()? {
|
|
||||||
VersionDiff::Latest(version) => {
|
|
||||||
self.widgets.game_version.set_label(&version.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionDiff::Predownload { current, latest, .. } => {
|
|
||||||
self.widgets.game_version.set_label(¤t.to_string());
|
|
||||||
self.widgets.game_version.set_css_classes(&["accent"]);
|
|
||||||
|
|
||||||
self.widgets.game_version.set_tooltip_text(Some(&format!("Game update pre-downloading available: {} -> {}", current, latest)));
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionDiff::Diff { current, latest, .. } => {
|
|
||||||
self.widgets.game_version.set_label(¤t.to_string());
|
|
||||||
self.widgets.game_version.set_css_classes(&["warning"]);
|
|
||||||
|
|
||||||
self.widgets.game_version.set_tooltip_text(Some(&format!("Game update available: {} -> {}", current, latest)));
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionDiff::Outdated { current, latest } => {
|
|
||||||
self.widgets.game_version.set_label(¤t.to_string());
|
|
||||||
self.widgets.game_version.set_css_classes(&["error"]);
|
|
||||||
|
|
||||||
self.widgets.game_version.set_tooltip_text(Some(&format!("Game is too outdated and can't be updated. Latest version: {latest}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionDiff::NotInstalled { .. } => {
|
|
||||||
self.widgets.game_version.set_label("not installed");
|
|
||||||
self.widgets.game_version.set_css_classes(&[]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update patch version
|
|
||||||
status_page.set_description(Some("Updating patch info..."));
|
|
||||||
|
|
||||||
let patch = Patch::try_fetch(config.patch.servers, consts::PATCH_FETCHING_TIMEOUT)?;
|
|
||||||
|
|
||||||
match patch {
|
|
||||||
Patch::NotAvailable => {
|
|
||||||
self.widgets.patch_version.set_label("not available");
|
|
||||||
self.widgets.patch_version.set_css_classes(&["error"]);
|
|
||||||
|
|
||||||
self.widgets.patch_version.set_tooltip_text(Some("Patch is not available"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Patch::Outdated { current, latest, .. } => {
|
|
||||||
self.widgets.patch_version.set_label(&format!("outdated ({})", current));
|
|
||||||
self.widgets.patch_version.set_css_classes(&["warning"]);
|
|
||||||
|
|
||||||
self.widgets.patch_version.set_tooltip_text(Some(&format!("Patch is outdated ({current} -> {latest})")));
|
|
||||||
}
|
|
||||||
|
|
||||||
Patch::Preparation { .. } => {
|
|
||||||
self.widgets.patch_version.set_label("preparation");
|
|
||||||
self.widgets.patch_version.set_css_classes(&["warning"]);
|
|
||||||
|
|
||||||
self.widgets.patch_version.set_tooltip_text(Some("Patch is in preparation state and will be available later"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Patch::Testing { version, .. } => {
|
|
||||||
self.widgets.patch_version.set_label(&version.to_string());
|
|
||||||
self.widgets.patch_version.set_css_classes(&["warning"]);
|
|
||||||
|
|
||||||
self.widgets.patch_version.set_tooltip_text(Some("Patch is in testing phase"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Patch::Available { version, .. } => {
|
|
||||||
self.widgets.patch_version.set_label(&version.to_string());
|
|
||||||
|
|
||||||
if let Ok(true) = patch.is_applied(&config.game.path) {
|
|
||||||
self.widgets.patch_version.set_css_classes(&["success"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
self.widgets.patch_version.set_css_classes(&["warning"]);
|
|
||||||
self.widgets.patch_version.set_tooltip_text(Some("Patch is not applied"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update downloaded wine versions
|
|
||||||
self.update(Actions::UpdateWineComboRow).unwrap();
|
|
||||||
|
|
||||||
// Update downloaded DXVK versions
|
|
||||||
self.update(Actions::UpdateDxvkComboRow).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Toast for App {
|
|
||||||
fn get_toast_widgets(&self) -> (adw::ApplicationWindow, adw::ToastOverlay) {
|
|
||||||
let app = (*self.app).take();
|
|
||||||
self.app.set(app.clone());
|
|
||||||
|
|
||||||
app.unwrap().get_toast_widgets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for App {}
|
|
||||||
unsafe impl Sync for App {}
|
|
||||||
|
|
62
src/ui/preferences/main.rs
Normal file
62
src/ui/preferences/main.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use relm4::prelude::*;
|
||||||
|
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use crate::i18n::tr;
|
||||||
|
|
||||||
|
pub struct App;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppMsg {
|
||||||
|
Test
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub)]
|
||||||
|
impl SimpleComponent for App {
|
||||||
|
type Init = gtk::Window;
|
||||||
|
type Input = AppMsg;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
preferences_window = adw::PreferencesWindow {
|
||||||
|
set_title: Some(&tr("preferences")),
|
||||||
|
set_default_size: (700, 560),
|
||||||
|
set_hide_on_close: true,
|
||||||
|
set_modal: true,
|
||||||
|
|
||||||
|
#[template]
|
||||||
|
add = &super::general::General,
|
||||||
|
|
||||||
|
#[template]
|
||||||
|
add = &super::enhancements::Enhancements,
|
||||||
|
|
||||||
|
connect_close_request => |_| {
|
||||||
|
anime_launcher_sdk::config::flush().unwrap(); // FIXME
|
||||||
|
|
||||||
|
gtk::Inhibit::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
parent: Self::Init,
|
||||||
|
root: &Self::Root,
|
||||||
|
_sender: ComponentSender<Self>,
|
||||||
|
) -> ComponentParts<Self> {
|
||||||
|
let model = App;
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
widgets.preferences_window.set_transient_for(Some(&parent));
|
||||||
|
|
||||||
|
ComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
|
||||||
|
match msg {
|
||||||
|
AppMsg::Test => {
|
||||||
|
println!("sus");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,102 +1,4 @@
|
||||||
use gtk::prelude::*;
|
pub mod main;
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::cell::Cell;
|
|
||||||
|
|
||||||
use crate::ui::*;
|
|
||||||
use crate::ui::traits::prelude::*;
|
|
||||||
|
|
||||||
mod general;
|
mod general;
|
||||||
mod enhancements;
|
mod enhancements;
|
||||||
mod environment;
|
|
||||||
|
|
||||||
pub mod gamescope;
|
|
||||||
|
|
||||||
pub mod pages {
|
|
||||||
pub use super::general::App as GeneralPage;
|
|
||||||
pub use super::enhancements::App as EnhancementsPage;
|
|
||||||
pub use super::environment::App as EnvironmentPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, glib::Downgrade)]
|
|
||||||
pub struct PreferencesStack {
|
|
||||||
pub app: Rc<Cell<Option<super::MainApp>>>,
|
|
||||||
|
|
||||||
pub preferences: gtk::Box,
|
|
||||||
pub preferences_go_back: gtk::Button,
|
|
||||||
|
|
||||||
pub status_page: adw::StatusPage,
|
|
||||||
pub flap: adw::Flap,
|
|
||||||
|
|
||||||
pub stack: gtk::Stack,
|
|
||||||
|
|
||||||
pub general_page: pages::GeneralPage,
|
|
||||||
pub enhancements_page: pages::EnhancementsPage,
|
|
||||||
pub environment_page: pages::EnvironmentPage
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PreferencesStack {
|
|
||||||
pub fn new(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
|
||||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences.ui");
|
|
||||||
|
|
||||||
let result = Self {
|
|
||||||
app: Default::default(),
|
|
||||||
|
|
||||||
preferences: get_object(&builder, "preferences")?,
|
|
||||||
preferences_go_back: get_object(&builder, "preferences_go_back")?,
|
|
||||||
|
|
||||||
status_page: get_object(&builder, "status_page")?,
|
|
||||||
flap: get_object(&builder, "flap")?,
|
|
||||||
|
|
||||||
stack: get_object(&builder, "stack")?,
|
|
||||||
|
|
||||||
general_page: pages::GeneralPage::new()?,
|
|
||||||
enhancements_page: pages::EnhancementsPage::new(window)?,
|
|
||||||
environment_page: pages::EnvironmentPage::new()?
|
|
||||||
};
|
|
||||||
|
|
||||||
result.stack.add_titled(&result.general_page.get_page(), None, &pages::GeneralPage::title());
|
|
||||||
result.stack.add_titled(&result.enhancements_page.get_page(), None, &pages::EnhancementsPage::title());
|
|
||||||
result.stack.add_titled(&result.environment_page.get_page(), None, &pages::EnvironmentPage::title());
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_app(&mut self, app: super::MainApp) {
|
|
||||||
self.app.set(Some(app.clone()));
|
|
||||||
|
|
||||||
self.general_page.set_app(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update page info before opening it
|
|
||||||
///
|
|
||||||
/// Being called from the `MainApp` struct
|
|
||||||
pub fn update(&self) -> anyhow::Result<()> {
|
|
||||||
self.status_page.show();
|
|
||||||
self.status_page.set_description(None);
|
|
||||||
self.flap.hide();
|
|
||||||
|
|
||||||
self.general_page.prepare(&self.status_page)?;
|
|
||||||
self.enhancements_page.prepare(&self.status_page)?;
|
|
||||||
self.environment_page.prepare(&self.status_page)?;
|
|
||||||
|
|
||||||
self.status_page.hide();
|
|
||||||
self.flap.show();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Toast for PreferencesStack {
|
|
||||||
fn get_toast_widgets(&self) -> (adw::ApplicationWindow, adw::ToastOverlay) {
|
|
||||||
let app = (*self.app).take();
|
|
||||||
self.app.set(app.clone());
|
|
||||||
|
|
||||||
app.unwrap().get_toast_widgets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl Send for PreferencesStack {}
|
|
||||||
unsafe impl Sync for PreferencesStack {}
|
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
use gtk::glib;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use anime_game_core::prelude::*;
|
|
||||||
use wait_not_await::Await;
|
|
||||||
|
|
||||||
use crate::lib::config;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DownloadingResult {
|
|
||||||
DownloadingError(DownloadingError),
|
|
||||||
UnpackingError(String),
|
|
||||||
Done
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DownloadComponent {
|
|
||||||
fn get_component_path<T: Into<PathBuf>>(&self, installation_path: T) -> PathBuf;
|
|
||||||
fn get_downloading_widgets(&self) -> (gtk::ProgressBar, gtk::Button);
|
|
||||||
fn get_download_uri(&self) -> String;
|
|
||||||
|
|
||||||
fn is_downloaded<T: Into<PathBuf>>(&self, installation_path: T) -> bool {
|
|
||||||
Path::new(&self.get_component_path(installation_path)).exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn download<T: Into<PathBuf>>(&self, installation_path: T) -> anyhow::Result<Await<DownloadingResult>> {
|
|
||||||
let (sender, receiver) = glib::MainContext::channel::<InstallerUpdate>(glib::PRIORITY_DEFAULT);
|
|
||||||
let (progress_bar, button) = self.get_downloading_widgets();
|
|
||||||
|
|
||||||
progress_bar.set_visible(true);
|
|
||||||
button.set_visible(false);
|
|
||||||
|
|
||||||
let (downl_send, downl_recv) = std::sync::mpsc::channel();
|
|
||||||
|
|
||||||
receiver.attach(None, move |state| {
|
|
||||||
match state {
|
|
||||||
InstallerUpdate::DownloadingStarted(_) => (),
|
|
||||||
InstallerUpdate::DownloadingFinished => (),
|
|
||||||
InstallerUpdate::UnpackingStarted(_) => (),
|
|
||||||
|
|
||||||
InstallerUpdate::CheckingFreeSpace(_) => {
|
|
||||||
progress_bar.set_text(Some("Checking free space..."));
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::DownloadingProgress(curr, total) => {
|
|
||||||
let progress = curr as f64 / total as f64;
|
|
||||||
|
|
||||||
progress_bar.set_fraction(progress);
|
|
||||||
progress_bar.set_text(Some(&format!("Downloading: {}%", (progress * 100.0) as u64)));
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::UnpackingProgress(curr, total) => {
|
|
||||||
let progress = curr as f64 / total as f64;
|
|
||||||
|
|
||||||
progress_bar.set_fraction(progress);
|
|
||||||
progress_bar.set_text(Some(&format!("Unpacking: {}%", (progress * 100.0) as u64)));
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::UnpackingFinished => {
|
|
||||||
progress_bar.set_visible(false);
|
|
||||||
button.set_visible(true);
|
|
||||||
|
|
||||||
downl_send.send(DownloadingResult::Done).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::DownloadingError(err) => {
|
|
||||||
downl_send.send(DownloadingResult::DownloadingError(err)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallerUpdate::UnpackingError(err) => {
|
|
||||||
downl_send.send(DownloadingResult::UnpackingError(err)).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glib::Continue(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
let (send, recv) = std::sync::mpsc::channel();
|
|
||||||
let config = config::get()?;
|
|
||||||
|
|
||||||
let mut installer = Installer::new(self.get_download_uri())?;
|
|
||||||
|
|
||||||
if let Some(temp_folder) = config.launcher.temp {
|
|
||||||
installer.temp_folder = temp_folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
installer.downloader
|
|
||||||
.set_downloading_speed(config.launcher.speed_limit)
|
|
||||||
.expect("Failed to set downloading speed limit");
|
|
||||||
|
|
||||||
send.send(installer).unwrap();
|
|
||||||
|
|
||||||
let installation_path = installation_path.into();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let mut installer = recv.recv().unwrap();
|
|
||||||
|
|
||||||
installer.install(installation_path, move |state| {
|
|
||||||
sender.send(state).unwrap();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Await::new(move || {
|
|
||||||
downl_recv.recv().unwrap()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete<T: Into<PathBuf>>(&self, installation_path: T) -> std::io::Result<()> {
|
|
||||||
std::fs::remove_dir_all(self.get_component_path(installation_path))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
pub mod toast;
|
|
||||||
pub mod download_component;
|
|
||||||
|
|
||||||
pub mod prelude {
|
|
||||||
pub use super::toast::*;
|
|
||||||
pub use super::download_component::*;
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
use gtk::prelude::*;
|
|
||||||
|
|
||||||
use crate::ui::add_action;
|
|
||||||
|
|
||||||
pub trait Toast {
|
|
||||||
fn get_toast_widgets(&self) -> (adw::ApplicationWindow, adw::ToastOverlay);
|
|
||||||
|
|
||||||
/// Show toast with `toast` title and `See message` button
|
|
||||||
///
|
|
||||||
/// This button will show message dialog with some message
|
|
||||||
fn toast<T: ToString, F: std::fmt::Display + 'static>(&self, toast: T, message: F) {
|
|
||||||
let toast = adw::Toast::new(toast.to_string().as_str());
|
|
||||||
let (window, toast_overlay) = self.get_toast_widgets();
|
|
||||||
|
|
||||||
toast.set_timeout(0);
|
|
||||||
|
|
||||||
let message = format!("{}", message);
|
|
||||||
|
|
||||||
if !message.is_empty() {
|
|
||||||
toast.set_button_label(Some("See message"));
|
|
||||||
toast.set_action_name(Some("see-message.see-message"));
|
|
||||||
|
|
||||||
// Show message in a dialog window
|
|
||||||
add_action(&toast_overlay, "see-message", move || {
|
|
||||||
let dialog = gtk::MessageDialog::new(
|
|
||||||
Some(&window),
|
|
||||||
gtk::DialogFlags::all(),
|
|
||||||
gtk::MessageType::Info,
|
|
||||||
gtk::ButtonsType::Close,
|
|
||||||
&message
|
|
||||||
);
|
|
||||||
|
|
||||||
dialog.connect_response(move |dialog, _| {
|
|
||||||
dialog.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toast_overlay.add_toast(&toast);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue