mirror of
https://github.com/an-anime-team/sleepy-launcher.git
synced 2024-11-21 20:27:35 +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
|
||||
/assets/ui/.dist
|
||||
|
||||
/scripts/builds
|
||||
/scripts/appimage/dist
|
||||
/scripts/appimage/*.AppImage
|
||||
/assets/locales/TODO.*
|
||||
|
|
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -1,9 +1,3 @@
|
|||
[submodule "blueprint-compiler"]
|
||||
path = blueprint-compiler
|
||||
url = https://gitlab.gnome.org/jwestman/blueprint-compiler
|
||||
[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
|
||||
[submodule "anime-launcher-sdk"]
|
||||
path = anime-launcher-sdk
|
||||
url = https://github.com/an-anime-team/anime-launcher-sdk
|
||||
|
|
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]
|
||||
name = "anime-game-launcher"
|
||||
version = "1.2.4"
|
||||
version = "2.0.0"
|
||||
description = "Anime Game launcher"
|
||||
authors = ["Nikita Podvirnyy <suimin.tu.mu.ga.mi@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
|
@ -16,19 +16,16 @@ opt-level = 3
|
|||
glib-build-tools = "0.16"
|
||||
|
||||
[dependencies]
|
||||
relm4 = { version = "0.5.0-rc.1", features = ["macros", "libadwaita"] }
|
||||
gtk = { package = "gtk4", version = "0.5", features = ["v4_8"] }
|
||||
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"] }
|
||||
wincompatlib = { version = "0.1.3", features = ["dxvk"] }
|
||||
anime-launcher-sdk = { path = "anime-launcher-sdk" }
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
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"
|
||||
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"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/app/assets">
|
||||
<gresource prefix="/org/app">
|
||||
<file compressed="true">images/icon.png</file>
|
||||
</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
|
87
build.rs
87
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() {
|
||||
blp_process_dir(String::new());
|
||||
|
||||
if read_to_string("assets/resources.xml").is_ok() {
|
||||
glib_build_tools::compile_resources(
|
||||
"assets",
|
||||
"assets/resources.xml",
|
||||
".assets.gresource",
|
||||
);
|
||||
}
|
||||
glib_build_tools::compile_resources(
|
||||
"assets",
|
||||
"assets/resources.xml",
|
||||
"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 gtk::gdk::Display;
|
||||
use gtk::glib;
|
||||
use gtk::glib::clone;
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use anime_launcher_sdk::config;
|
||||
|
||||
pub mod i18n;
|
||||
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() {
|
||||
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");
|
||||
|
||||
// Register and include resources
|
||||
gtk::gio::resources_register_include!(".assets.gresource")
|
||||
gtk::gio::resources_register_include!("resources.gresource")
|
||||
.expect("Failed to register resources");
|
||||
|
||||
// Set application's title
|
||||
glib::set_application_name("An Anime Game Launcher");
|
||||
glib::set_program_name(Some("An Anime Game Launcher"));
|
||||
gtk::glib::set_application_name("An Anime Game Launcher");
|
||||
gtk::glib::set_program_name(Some("An Anime Game Launcher"));
|
||||
|
||||
// Create app
|
||||
let application = gtk::Application::new(
|
||||
Some(APP_ID),
|
||||
Default::default()
|
||||
);
|
||||
// Set UI language
|
||||
unsafe {
|
||||
i18n::LANG = config::get().unwrap().launcher.language.parse().unwrap();
|
||||
}
|
||||
|
||||
application.add_main_option(
|
||||
"run-game",
|
||||
glib::Char::from(0),
|
||||
glib::OptionFlags::empty(),
|
||||
glib::OptionArg::None,
|
||||
"Run the game",
|
||||
None
|
||||
);
|
||||
// Run the app
|
||||
let app = RelmApp::new("moe.launcher.an-anime-game-launcher");
|
||||
|
||||
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_game.set(true);
|
||||
}
|
||||
|
||||
-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();
|
||||
app.run::<ui::main::App>(());
|
||||
}
|
||||
|
|
|
@ -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")?
|
||||
})
|
||||
}
|
||||
}
|
1109
src/ui/main.rs
1109
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 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 gtk::glib;
|
||||
use gtk::glib::clone;
|
||||
use anime_launcher_sdk::config;
|
||||
use anime_launcher_sdk::config::prelude::*;
|
||||
|
||||
use crate::lib;
|
||||
use crate::lib::config;
|
||||
use crate::lib::config::prelude::*;
|
||||
use crate::i18n::tr;
|
||||
use crate::ui::main::is_ready;
|
||||
|
||||
use crate::ui::*;
|
||||
|
||||
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
|
||||
lazy_static::lazy_static! {
|
||||
static ref CONFIG: config::Config = config::get().expect("Failed to load config");
|
||||
}
|
||||
|
||||
impl AppWidgets {
|
||||
fn try_get(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences/enhancements.ui");
|
||||
#[relm4::widget_template(pub)]
|
||||
impl WidgetTemplate for Enhancements {
|
||||
view! {
|
||||
adw::PreferencesPage {
|
||||
set_title: &tr("enhancements"),
|
||||
set_icon_name: Some("applications-graphics-symbolic"),
|
||||
|
||||
let result = Self {
|
||||
page: get_object(&builder, "page")?,
|
||||
add = &adw::PreferencesGroup {
|
||||
set_title: &tr("wine"),
|
||||
|
||||
sync_combo: get_object(&builder, "sync_combo")?,
|
||||
wine_lang: get_object(&builder, "wine_lang")?,
|
||||
borderless: get_object(&builder, "borderless")?,
|
||||
virtual_desktop_row: get_object(&builder, "virtual_desktop_row")?,
|
||||
virtual_desktop: get_object(&builder, "virtual_desktop")?,
|
||||
adw::ComboRow {
|
||||
set_title: &tr("synchronization"),
|
||||
set_subtitle: &tr("wine-sync-description"),
|
||||
|
||||
hud_combo: get_object(&builder, "hud_combo")?,
|
||||
fsr_combo: get_object(&builder, "fsr_combo")?,
|
||||
fsr_switcher: get_object(&builder, "fsr_switcher")?,
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
&tr("none"),
|
||||
"ESync",
|
||||
"FSync",
|
||||
"Futex2"
|
||||
]),
|
||||
|
||||
gamemode_row: get_object(&builder, "gamemode_row")?,
|
||||
gamemode_switcher: get_object(&builder, "gamemode_switcher")?,
|
||||
set_selected: CONFIG.game.wine.sync.into(),
|
||||
|
||||
gamescope_row: get_object(&builder, "gamescope_row")?,
|
||||
gamescope_settings: get_object(&builder, "gamescope_settings")?,
|
||||
gamescope_switcher: get_object(&builder, "gamescope_switcher")?,
|
||||
connect_selected_notify => move |row| {
|
||||
if is_ready() {
|
||||
if let Ok(mut config) = config::get() {
|
||||
config.game.wine.sync = WineSync::try_from(row.selected()).unwrap();
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
gamescope_app: GamescopeApp::new(window)?,
|
||||
adw::ComboRow {
|
||||
set_title: &tr("language"),
|
||||
set_subtitle: &tr("wine-lang-description"),
|
||||
|
||||
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")?
|
||||
};
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
&tr("system"),
|
||||
"English",
|
||||
"Русский",
|
||||
"Deutsch",
|
||||
"Português",
|
||||
"Polska",
|
||||
"Français",
|
||||
"Español",
|
||||
"中国",
|
||||
"日本語",
|
||||
"한국어"
|
||||
]),
|
||||
|
||||
// Set availale wine languages
|
||||
result.wine_lang.set_model(Some(&WineLang::get_model()));
|
||||
set_selected: CONFIG.game.wine.language.into(),
|
||||
|
||||
// Set availale virtual desktop resolutions
|
||||
result.virtual_desktop_row.set_model(Some(&Resolution::get_model()));
|
||||
connect_selected_notify => move |row| {
|
||||
if is_ready() {
|
||||
if let Ok(mut config) = config::get() {
|
||||
config.game.wine.language = WineLang::try_from(row.selected()).unwrap();
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Set availale fps unlocker limits
|
||||
result.fps_unlocker_combo.set_model(Some(&Fps::get_model()));
|
||||
adw::ActionRow {
|
||||
set_title: &tr("borderless-window"),
|
||||
|
||||
// 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"));
|
||||
}
|
||||
add_suffix = >k::Switch {
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
},
|
||||
|
||||
// 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"));
|
||||
}
|
||||
adw::ComboRow {
|
||||
set_title: &tr("virtual-desktop"),
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
&tr("custom"),
|
||||
"960x540",
|
||||
"1280x720",
|
||||
"1920x1080",
|
||||
"2560x1440",
|
||||
"3840x2160"
|
||||
]),
|
||||
|
||||
/// 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
|
||||
}
|
||||
set_selected: CONFIG.game.wine.virtual_desktop.get_resolution().into(),
|
||||
|
||||
impl App {
|
||||
/// Create new application
|
||||
pub fn new(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
|
||||
let result = Self {
|
||||
widgets: AppWidgets::try_get(window)?
|
||||
}.init_events();
|
||||
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();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
config.game.wine.virtual_desktop.width = width;
|
||||
config.game.wine.virtual_desktop.height = height;
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/// 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() {
|
||||
config.game.wine.sync = WineSync::try_from(row.selected()).unwrap();
|
||||
add_suffix = >k::Switch {
|
||||
set_valign: gtk::Align::Center,
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
});
|
||||
set_state: CONFIG.game.wine.virtual_desktop.enabled,
|
||||
|
||||
// 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
|
||||
self.widgets.virtual_desktop_row.connect_selected_notify(move |row| {
|
||||
if let Ok(mut config) = config::get() {
|
||||
let resolutions = Resolution::list();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Virtual desktop switching
|
||||
self.widgets.virtual_desktop.connect_state_notify(move |switch| {
|
||||
if let Ok(mut config) = config::get() {
|
||||
config.game.wine.virtual_desktop.enabled = switch.state();
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
connect_state_notify => move |switch| {
|
||||
if is_ready() {
|
||||
if let Ok(mut config) = config::get() {
|
||||
config.game.wine.virtual_desktop.enabled = switch.state();
|
||||
|
||||
// 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);
|
||||
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();
|
||||
add = &adw::PreferencesGroup {
|
||||
set_title: &tr("game"),
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
});
|
||||
adw::ComboRow {
|
||||
set_title: &tr("hud"),
|
||||
|
||||
// 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;
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
&tr("none"),
|
||||
"DXVK",
|
||||
"MangoHud"
|
||||
]),
|
||||
},
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
});
|
||||
adw::ComboRow {
|
||||
set_title: &tr("fsr"),
|
||||
set_subtitle: &tr("fsr-description"),
|
||||
|
||||
// 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;
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
&tr("ultra-quality"),
|
||||
&tr("quality"),
|
||||
&tr("balanced"),
|
||||
&tr("performance")
|
||||
]),
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
});
|
||||
add_suffix = >k::Switch {
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
},
|
||||
|
||||
// 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;
|
||||
adw::ActionRow {
|
||||
set_title: &tr("gamemode"),
|
||||
set_subtitle: &tr("gamemode-description"),
|
||||
|
||||
config::update(config);
|
||||
}
|
||||
});
|
||||
add_suffix = >k::Switch {
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
self
|
||||
}
|
||||
add = &adw::PreferencesGroup {
|
||||
set_title: &tr("fps-unlocker"),
|
||||
|
||||
pub fn title() -> String {
|
||||
String::from("Enhancements")
|
||||
}
|
||||
adw::ComboRow {
|
||||
set_title: &tr("enabled"),
|
||||
set_subtitle: &tr("fps-unlocker-description"),
|
||||
|
||||
pub fn get_page(&self) -> adw::PreferencesPage {
|
||||
self.widgets.page.clone()
|
||||
}
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
&tr("custom"),
|
||||
"90",
|
||||
"120",
|
||||
"144",
|
||||
"165",
|
||||
"180",
|
||||
"200",
|
||||
"240"
|
||||
]),
|
||||
|
||||
/// This method is being called by the `PreferencesStack::update`
|
||||
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
|
||||
let config = config::get()?;
|
||||
add_suffix = >k::Switch {
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
},
|
||||
|
||||
status_page.set_description(Some("Loading enhancements..."));
|
||||
adw::ActionRow {
|
||||
set_title: &tr("power-saving"),
|
||||
set_subtitle: &tr("power-saving-description"),
|
||||
|
||||
// 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);
|
||||
add_suffix = >k::Switch {
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
for (i, value) in Fps::list().into_iter().enumerate() {
|
||||
if value == fps {
|
||||
self.widgets.fps_unlocker_combo.set_selected(i as u32 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 adw::prelude::*;
|
||||
|
||||
use gtk::glib;
|
||||
use gtk::glib::clone;
|
||||
use anime_launcher_sdk::config;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::cell::Cell;
|
||||
use crate::i18n::tr;
|
||||
use crate::ui::main::is_ready;
|
||||
|
||||
use anime_game_core::prelude::*;
|
||||
use anime_game_core::genshin::prelude::*;
|
||||
|
||||
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>>
|
||||
lazy_static::lazy_static! {
|
||||
static ref CONFIG: config::Config = config::get().expect("Failed to load config");
|
||||
}
|
||||
|
||||
impl AppWidgets {
|
||||
pub fn try_get() -> anyhow::Result<Self> {
|
||||
let builder = gtk::Builder::from_resource("/org/app/ui/preferences/general.ui");
|
||||
#[relm4::widget_template(pub)]
|
||||
impl WidgetTemplate for General {
|
||||
view! {
|
||||
adw::PreferencesPage {
|
||||
set_title: &tr("general"),
|
||||
set_icon_name: Some("applications-system-symbolic"),
|
||||
|
||||
let mut result = Self {
|
||||
page: get_object(&builder, "page")?,
|
||||
add = &adw::PreferencesGroup {
|
||||
set_title: &tr("general"),
|
||||
|
||||
voiceovers_row: get_object(&builder, "voiceovers_row")?,
|
||||
voieover_components: Default::default(),
|
||||
adw::ComboRow {
|
||||
set_title: &tr("launcher-language"),
|
||||
|
||||
repair_game: get_object(&builder, "repair_game")?,
|
||||
// TODO: maybe simplify it by some way? e.g. specify such stuff in i18n mod
|
||||
|
||||
game_version: get_object(&builder, "game_version")?,
|
||||
patch_version: get_object(&builder, "patch_version")?,
|
||||
#[wrap(Some)]
|
||||
set_model = >k::StringList::new(&[
|
||||
"English",
|
||||
"Русский"
|
||||
]),
|
||||
|
||||
wine_selected: get_object(&builder, "wine_selected")?,
|
||||
set_selected: match CONFIG.launcher.language.as_str() {
|
||||
"en-us" => 0,
|
||||
"ru-ru" => 1,
|
||||
_ => 0
|
||||
},
|
||||
|
||||
wine_groups: get_object(&builder, "wine_groups")?,
|
||||
wine_recommended_only: get_object(&builder, "wine_recommended_only")?,
|
||||
|
||||
wine_components: Default::default(),
|
||||
|
||||
dxvk_selected: get_object(&builder, "dxvk_selected")?,
|
||||
|
||||
dxvk_groups: get_object(&builder, "dxvk_groups")?,
|
||||
dxvk_recommended_only: get_object(&builder, "dxvk_recommended_only")?,
|
||||
|
||||
dxvk_components: Default::default()
|
||||
};
|
||||
|
||||
let config = config::get()?;
|
||||
|
||||
// Update voiceovers list
|
||||
let voice_packages = VoicePackage::list_latest()?;
|
||||
|
||||
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");
|
||||
connect_selected_notify => move |row| {
|
||||
if is_ready() {
|
||||
if let Ok(mut config) = config::get() {
|
||||
config.launcher.language = String::from(*[
|
||||
"en-us",
|
||||
"ru-ru"
|
||||
].get(row.selected() as usize).unwrap_or(&"en-us"));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Actions::UpdateWineComboRow => {
|
||||
let model = gtk::StringList::new(&["System"]);
|
||||
|
||||
let list = wine::List::list_downloaded(config.game.wine.builds)
|
||||
.expect("Failed to list downloaded wine versions");
|
||||
|
||||
let mut selected = 0;
|
||||
|
||||
for (i, version) in list.iter().enumerate() {
|
||||
model.append(version.title.as_str());
|
||||
|
||||
if let Some(curr) = &config.game.wine.selected {
|
||||
if &version.name == curr {
|
||||
selected = i as u32 + 1;
|
||||
config::update(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
let mut values = this.values.take();
|
||||
adw::ExpanderRow {
|
||||
set_title: &tr("game-voiceovers"),
|
||||
|
||||
values.downloaded_wine_versions = Some(list);
|
||||
add_row = &adw::ActionRow {
|
||||
set_title: &tr("english"),
|
||||
|
||||
this.values.set(values);
|
||||
add_suffix = >k::Button {
|
||||
add_css_class: "flat",
|
||||
set_icon_name: "user-trash-symbolic",
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
},
|
||||
|
||||
// This will prevent SelectWineVersion action to be invoked
|
||||
let guard = this.widgets.wine_selected.freeze_notify();
|
||||
add_row = &adw::ActionRow {
|
||||
set_title: &tr("japanese"),
|
||||
|
||||
// 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);
|
||||
add_suffix = >k::Button {
|
||||
add_css_class: "flat",
|
||||
set_icon_name: "user-trash-symbolic",
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
},
|
||||
|
||||
drop(guard);
|
||||
}
|
||||
add_row = &adw::ActionRow {
|
||||
set_title: &tr("korean"),
|
||||
|
||||
Actions::SelectWineVersion(i) => {
|
||||
let values = this.values.take();
|
||||
add_suffix = >k::Button {
|
||||
add_css_class: "flat",
|
||||
set_icon_name: "user-trash-symbolic",
|
||||
set_valign: gtk::Align::Center
|
||||
}
|
||||
},
|
||||
|
||||
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())
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
this.values.set(values);
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 8,
|
||||
set_margin_top: 16,
|
||||
|
||||
config::update(config);
|
||||
gtk::Button {
|
||||
set_label: &tr("repair-game")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Actions::Toast(toast) => {
|
||||
let (msg, err) = (toast.0.clone(), toast.1.to_string());
|
||||
add = &adw::PreferencesGroup {
|
||||
set_title: &tr("status"),
|
||||
|
||||
this.toast(msg, err);
|
||||
}
|
||||
}
|
||||
adw::ActionRow {
|
||||
set_title: &tr("game-version"),
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
add_suffix = >k::Label {
|
||||
set_text: "3.3.0",
|
||||
add_css_class: "success"
|
||||
}
|
||||
},
|
||||
|
||||
self.actions.set(Some(sender));
|
||||
adw::ActionRow {
|
||||
set_title: &tr("patch-version"),
|
||||
|
||||
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"));
|
||||
add_suffix = >k::Label {
|
||||
set_text: "3.3.0",
|
||||
add_css_class: "success"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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::*;
|
||||
|
||||
use gtk::glib;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::ui::*;
|
||||
use crate::ui::traits::prelude::*;
|
||||
pub mod main;
|
||||
|
||||
mod general;
|
||||
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…
Reference in a new issue