relm4 init commit

This commit is contained in:
Observer KRypt0n_ 2023-01-18 18:37:53 +02:00
parent da37ea2103
commit d6b5eb6411
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
87 changed files with 1333 additions and 8854 deletions

6
.gitignore vendored
View file

@ -1,6 +1,2 @@
/target
/assets/ui/.dist
/scripts/builds
/scripts/appimage/dist
/scripts/appimage/*.AppImage
/assets/locales/TODO.*

12
.gitmodules vendored
View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

@ -0,0 +1 @@
Subproject commit f85152c83a715957b553e5fb1f2c6b2da93f2f89

View file

@ -1,7 +0,0 @@
[Desktop Entry]
Name=An Anime Game Launcher GTK
Icon=icon
Exec=AppRun
Type=Application
Categories=Game
Terminal=false

View 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)

View 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 и снижать приоритет процесса игры когда она не находится в фокусе

View file

@ -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>

View file

@ -1,3 +0,0 @@
progressbar > text {
margin-bottom: 4px;
}

View file

@ -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;
}
}
};
}

View file

@ -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";
}
}
}
}
}

View file

@ -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";
}
}
}
}
}

View file

@ -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";
}
}
}
}
}

View file

@ -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";
}
}
}
}
}

View file

@ -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";
}
}
}
}
}

View file

@ -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";
}
}
}
}
}

View file

@ -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";
}
}
}
}
}

View file

@ -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")
}
}

View file

@ -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 {};
}
}

View file

@ -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"
]
};
}
}
}

View file

@ -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";
}
}

View file

@ -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"]
}
}
}
}
}

View file

@ -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

View file

@ -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",
"resources.gresource",
);
}
}

@ -1 +0,0 @@
Subproject commit 5580f7be0fbdfba677ec32b2fd7d11cb762edebf

19
src/i18n.rs Normal file
View 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")
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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()
}
}

View file

@ -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")
])
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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))
}
}

View file

@ -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")])
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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}"))
}
}

View file

@ -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"))
}

View file

@ -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())
}
}
}

View file

@ -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)
}
}

View file

@ -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()?
)?)
}
}

View file

@ -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(())
}

View file

@ -1 +0,0 @@
pub mod states;

View file

@ -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)
})
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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()
);
application.add_main_option(
"run-game",
glib::Char::from(0),
glib::OptionFlags::empty(),
glib::OptionArg::None,
"Run the game",
None
);
application.add_main_option(
"just-run-game",
glib::Char::from(0),
glib::OptionFlags::empty(),
glib::OptionArg::None,
"Run the game whenever it possible, ignoring updates predownloads",
None
);
let run_game = std::rc::Rc::new(std::cell::Cell::new(false));
let just_run_game = std::rc::Rc::new(std::cell::Cell::new(false));
application.connect_handle_local_options(clone!(@strong run_game, @strong just_run_game => move |_, arg| {
if arg.contains("just-run-game") {
just_run_game.set(true);
// Set UI language
unsafe {
i18n::LANG = config::get().unwrap().launcher.language.parse().unwrap();
}
else if arg.contains("run-game") {
run_game.set(true);
}
// Run the app
let app = RelmApp::new("moe.launcher.an-anime-game-launcher");
-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>(());
}

View file

@ -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);
}
}
}

View file

@ -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 {}

View file

@ -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;

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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);
}
}
}

View file

@ -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 {}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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]
}
}

View file

@ -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")?
})
}
}

View file

@ -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: &gtk::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 {}

View file

@ -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")?
})
}
}

View file

@ -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
}
}

View file

@ -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")?
})
}
}

File diff suppressed because it is too large Load diff

View file

@ -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: &gtk::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);
}

View file

@ -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 = &gtk::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")?,
gamescope_app: GamescopeApp::new(window)?,
fps_unlocker_combo: get_object(&builder, "fps_unlocker_combo")?,
fps_unlocker_switcher: get_object(&builder, "fps_unlocker_switcher")?,
fps_unlocker_power_saving_switcher: get_object(&builder, "fps_unlocker_power_saving_switcher")?,
fps_unlocker_monitor_num: get_object(&builder, "fps_unlocker_monitor_num")?,
fps_unlocker_window_mode_combo: get_object(&builder, "fps_unlocker_window_mode_combo")?,
fps_unlocker_priority_combo: get_object(&builder, "fps_unlocker_priority_combo")?
};
// Set availale wine languages
result.wine_lang.set_model(Some(&WineLang::get_model()));
// Set availale virtual desktop resolutions
result.virtual_desktop_row.set_model(Some(&Resolution::get_model()));
// Set availale fps unlocker limits
result.fps_unlocker_combo.set_model(Some(&Fps::get_model()));
// Disable gamemode row if it's not available
if !lib::is_available("gamemoderun") {
result.gamemode_row.set_sensitive(false);
result.gamemode_row.set_tooltip_text(Some("Gamemode is not installed"));
}
// Disable gamescope row if it's not available
if !lib::is_available("gamescope") {
result.gamescope_row.set_sensitive(false);
result.gamescope_row.set_tooltip_text(Some("Gamescope is not installed"));
}
Ok(result)
}
}
/// The main application structure
///
/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets
///
/// `Rc<Cell<Values>>` means this:
/// - `Rc` addeds ability to reference the same value from various clones of the structure.
/// This will guarantee us that inner `Cell<Values>` is the same for all the `App::clone()` values
/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference.
///
/// So we have a shared reference to some value that can be changed without mutable reference.
/// That's what we need and what we use in `App::update` method
#[derive(Clone, glib::Downgrade)]
pub struct App {
widgets: AppWidgets
}
impl App {
/// Create new application
pub fn new(window: &adw::ApplicationWindow) -> anyhow::Result<Self> {
let result = Self {
widgets: AppWidgets::try_get(window)?
}.init_events();
Ok(result)
}
/// Add default events and values to the widgets
fn init_events(self) -> Self {
// Wine sync selection
self.widgets.sync_combo.connect_selected_notify(move |row| {
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);
}
});
// 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| {
adw::ComboRow {
set_title: &tr("language"),
set_subtitle: &tr("wine-lang-description"),
#[wrap(Some)]
set_model = &gtk::StringList::new(&[
&tr("system"),
"English",
"Русский",
"Deutsch",
"Português",
"Polska",
"Français",
"Español",
"中国",
"日本語",
"한국어"
]),
set_selected: CONFIG.game.wine.language.into(),
connect_selected_notify => move |row| {
if is_ready() {
if let Ok(mut config) = config::get() {
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.game.wine.language = WineLang::try_from(row.selected()).unwrap();
config::update(config);
}
}
});
}
},
// Virtual desktop switching
self.widgets.virtual_desktop.connect_state_notify(move |switch| {
adw::ActionRow {
set_title: &tr("borderless-window"),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center
}
},
adw::ComboRow {
set_title: &tr("virtual-desktop"),
#[wrap(Some)]
set_model = &gtk::StringList::new(&[
&tr("custom"),
"960x540",
"1280x720",
"1920x1080",
"2560x1440",
"3840x2160"
]),
set_selected: CONFIG.game.wine.virtual_desktop.get_resolution().into(),
connect_selected_notify => move |row| {
if is_ready() {
if let Ok(mut config) = config::get() {
let (width, height) = Resolution::try_from(row.selected()).unwrap().get_pair();
config.game.wine.virtual_desktop.width = width;
config.game.wine.virtual_desktop.height = height;
config::update(config);
}
}
},
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center,
set_state: CONFIG.game.wine.virtual_desktop.enabled,
connect_state_notify => move |switch| {
if is_ready() {
if let Ok(mut config) = config::get() {
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);
}
});
// Gamemode switching
self.widgets.gamemode_switcher.connect_state_notify(move |switch| {
if let Ok(mut config) = config::get() {
config.game.enhancements.gamemode = switch.state();
config::update(config);
}
});
// Gamescope settings app
self.widgets.gamescope_settings.connect_clicked(clone!(@weak self as this => move |_| {
this.widgets.gamescope_app.show();
}));
// Gamescope swithing
self.widgets.gamescope_switcher.connect_state_notify(move |switch| {
if let Ok(mut config) = config::get() {
config.game.enhancements.gamescope.enabled = switch.state();
config::update(config);
}
});
// FPS unlocker swithing
self.widgets.fps_unlocker_switcher.connect_state_notify(move |switch| {
if let Ok(mut config) = config::get() {
config.game.enhancements.fps_unlocker.enabled = switch.state();
config::update(config);
}
});
// FPS unlocker -> fps limit combo
self.widgets.fps_unlocker_combo.connect_selected_notify(move |row| {
if let Ok(mut config) = config::get() {
if row.selected() > 0 {
config.game.enhancements.fps_unlocker.config.fps = Fps::list()[row.selected() as usize - 1].to_num();
config::update(config);
}
}
});
// FPS unlocker -> power saving swithing
self.widgets.fps_unlocker_power_saving_switcher.connect_state_notify(move |switch| {
if let Ok(mut config) = config::get() {
config.game.enhancements.fps_unlocker.config.power_saving = switch.state();
config::update(config);
}
});
// FPS unlocker -> monitor number
self.widgets.fps_unlocker_monitor_num.connect_changed(move |button| {
if let Ok(mut config) = config::get() {
config.game.enhancements.fps_unlocker.config.monitor = button.value() as u64;
config::update(config);
}
});
// FPS unlocker -> window mode combo
self.widgets.fps_unlocker_window_mode_combo.connect_selected_notify(move |row| {
if let Ok(mut config) = config::get() {
config.game.enhancements.fps_unlocker.config.window_mode = row.selected() as u64;
config::update(config);
}
});
// FPS unlocker -> priority combo
self.widgets.fps_unlocker_priority_combo.connect_selected_notify(move |row| {
if let Ok(mut config) = config::get() {
config.game.enhancements.fps_unlocker.config.priority = row.selected() as u64;
config::update(config);
}
});
self
}
pub fn title() -> String {
String::from("Enhancements")
}
pub fn get_page(&self) -> adw::PreferencesPage {
self.widgets.page.clone()
}
/// This method is being called by the `PreferencesStack::update`
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
let config = config::get()?;
status_page.set_description(Some("Loading enhancements..."));
// Update Wine sync
self.widgets.sync_combo.set_selected(config.game.wine.sync.into());
// Update wine language
self.widgets.wine_lang.set_selected(config.game.wine.language.into());
// Update borderless
self.widgets.borderless.set_state(config.game.wine.borderless);
// Update virtual desktop
self.widgets.virtual_desktop.set_state(config.game.wine.virtual_desktop.enabled);
let resolution = Resolution::from_pair(
config.game.wine.virtual_desktop.width,
config.game.wine.virtual_desktop.height
);
if let Resolution::Custom(_, _) = resolution {
self.widgets.virtual_desktop_row.set_selected(0);
}
else {
for (i, res) in Resolution::list().into_iter().enumerate() {
if res == resolution {
self.widgets.virtual_desktop_row.set_selected(i as u32 + 1);
}
}
}
// Update HUD
self.widgets.hud_combo.set_selected(config.game.enhancements.hud.into());
// FSR strength selection
self.widgets.fsr_combo.set_selected(5 - config.game.enhancements.fsr.strength as u32);
// FSR switching
self.widgets.fsr_switcher.set_state(config.game.enhancements.fsr.enabled);
// Gamemode switching
self.widgets.gamemode_switcher.set_state(config.game.enhancements.gamemode);
// Switch gamescope option
self.widgets.gamescope_switcher.set_state(config.game.enhancements.gamescope.enabled);
// Switch FPS unlocker
self.widgets.fps_unlocker_switcher.set_state(config.game.enhancements.fps_unlocker.enabled);
// Select FPS limit
let fps = Fps::from_num(config.game.enhancements.fps_unlocker.config.fps);
if let Fps::Custom(_) = fps {
self.widgets.fps_unlocker_combo.set_selected(0);
}
},
else {
for (i, value) in Fps::list().into_iter().enumerate() {
if value == fps {
self.widgets.fps_unlocker_combo.set_selected(i as u32 + 1);
add = &adw::PreferencesGroup {
set_title: &tr("game"),
adw::ComboRow {
set_title: &tr("hud"),
#[wrap(Some)]
set_model = &gtk::StringList::new(&[
&tr("none"),
"DXVK",
"MangoHud"
]),
},
adw::ComboRow {
set_title: &tr("fsr"),
set_subtitle: &tr("fsr-description"),
#[wrap(Some)]
set_model = &gtk::StringList::new(&[
&tr("ultra-quality"),
&tr("quality"),
&tr("balanced"),
&tr("performance")
]),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center
}
},
adw::ActionRow {
set_title: &tr("gamemode"),
set_subtitle: &tr("gamemode-description"),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center
}
}
},
add = &adw::PreferencesGroup {
set_title: &tr("fps-unlocker"),
adw::ComboRow {
set_title: &tr("enabled"),
set_subtitle: &tr("fps-unlocker-description"),
#[wrap(Some)]
set_model = &gtk::StringList::new(&[
&tr("custom"),
"90",
"120",
"144",
"165",
"180",
"200",
"240"
]),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center
}
},
adw::ActionRow {
set_title: &tr("power-saving"),
set_subtitle: &tr("power-saving-description"),
add_suffix = &gtk::Switch {
set_valign: gtk::Align::Center
}
}
}
}
// Switch FPS unlocker -> power saving
self.widgets.fps_unlocker_power_saving_switcher.set_state(config.game.enhancements.fps_unlocker.config.power_saving);
// Switch FPS unlocker -> monitor number
self.widgets.fps_unlocker_monitor_num.set_value(config.game.enhancements.fps_unlocker.config.monitor as f64);
// Switch FPS unlocker -> window mode
self.widgets.fps_unlocker_window_mode_combo.set_selected(config.game.enhancements.fps_unlocker.config.window_mode as u32);
// Switch FPS unlocker -> priority
self.widgets.fps_unlocker_priority_combo.set_selected(config.game.enhancements.fps_unlocker.config.priority as u32);
// Prepare gamescope settings app
self.widgets.gamescope_app.prepare(status_page)?;
Ok(())
}
}
unsafe impl Send for App {}
unsafe impl Sync for App {}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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");
let mut result = Self {
page: get_object(&builder, "page")?,
voiceovers_row: get_object(&builder, "voiceovers_row")?,
voieover_components: Default::default(),
repair_game: get_object(&builder, "repair_game")?,
game_version: get_object(&builder, "game_version")?,
patch_version: get_object(&builder, "patch_version")?,
wine_selected: get_object(&builder, "wine_selected")?,
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");
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);
#[relm4::widget_template(pub)]
impl WidgetTemplate for General {
view! {
adw::PreferencesPage {
set_title: &tr("general"),
set_icon_name: Some("applications-system-symbolic"),
add = &adw::PreferencesGroup {
set_title: &tr("general"),
adw::ComboRow {
set_title: &tr("launcher-language"),
// TODO: maybe simplify it by some way? e.g. specify such stuff in i18n mod
#[wrap(Some)]
set_model = &gtk::StringList::new(&[
"English",
"Русский"
]),
set_selected: match CONFIG.launcher.language.as_str() {
"en-us" => 0,
"ru-ru" => 1,
_ => 0
},
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"));
config::update(config);
}
}
}
},
Actions::UpdateWineComboRow => {
let model = gtk::StringList::new(&["System"]);
adw::ExpanderRow {
set_title: &tr("game-voiceovers"),
let list = wine::List::list_downloaded(config.game.wine.builds)
.expect("Failed to list downloaded wine versions");
add_row = &adw::ActionRow {
set_title: &tr("english"),
let mut selected = 0;
add_suffix = &gtk::Button {
add_css_class: "flat",
set_icon_name: "user-trash-symbolic",
set_valign: gtk::Align::Center
}
},
for (i, version) in list.iter().enumerate() {
model.append(version.title.as_str());
add_row = &adw::ActionRow {
set_title: &tr("japanese"),
if let Some(curr) = &config.game.wine.selected {
if &version.name == curr {
selected = i as u32 + 1;
add_suffix = &gtk::Button {
add_css_class: "flat",
set_icon_name: "user-trash-symbolic",
set_valign: gtk::Align::Center
}
},
add_row = &adw::ActionRow {
set_title: &tr("korean"),
add_suffix = &gtk::Button {
add_css_class: "flat",
set_icon_name: "user-trash-symbolic",
set_valign: gtk::Align::Center
}
},
add_row = &adw::ActionRow {
set_title: &tr("chinese"),
add_suffix = &gtk::Button {
add_css_class: "flat",
set_icon_name: "user-trash-symbolic",
set_valign: gtk::Align::Center
}
}
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 8,
set_margin_top: 16,
gtk::Button {
set_label: &tr("repair-game")
}
}
},
add = &adw::PreferencesGroup {
set_title: &tr("status"),
adw::ActionRow {
set_title: &tr("game-version"),
add_suffix = &gtk::Label {
set_text: "3.3.0",
add_css_class: "success"
}
},
adw::ActionRow {
set_title: &tr("patch-version"),
add_suffix = &gtk::Label {
set_text: "3.3.0",
add_css_class: "success"
}
}
}
let mut values = this.values.take();
values.downloaded_wine_versions = Some(list);
this.values.set(values);
// This will prevent SelectWineVersion action to be invoked
let guard = this.widgets.wine_selected.freeze_notify();
// We need to return app values before we call these methods
// because they'll invoke SelectWineVersion action so access
// downloaded_wine_versions value
this.widgets.wine_selected.set_model(Some(&model));
this.widgets.wine_selected.set_selected(selected);
drop(guard);
}
Actions::SelectWineVersion(i) => {
let values = this.values.take();
if let Some(wine_versions) = &values.downloaded_wine_versions {
match *i {
0 => config.game.wine.selected = None,
i => config.game.wine.selected = Some(wine_versions[i - 1].name.clone())
}
}
this.values.set(values);
config::update(config);
}
Actions::Toast(toast) => {
let (msg, err) = (toast.0.clone(), toast.1.to_string());
this.toast(msg, err);
}
}
glib::Continue(true)
});
self.actions.set(Some(sender));
self
}
/// Update widgets state by calling some action
pub fn update(&self, action: Actions) -> Result<(), std::sync::mpsc::SendError<Actions>> {
let actions = self.actions.take();
let result = match &actions {
Some(sender) => Ok(sender.send(action)?),
None => Ok(())
};
self.actions.set(actions);
result
}
pub fn title() -> String {
String::from("General")
}
pub fn get_page(&self) -> adw::PreferencesPage {
self.widgets.page.clone()
}
/// This method is being called by the `PreferencesStack::update`
pub fn prepare(&self, status_page: &adw::StatusPage) -> anyhow::Result<()> {
let config = config::get()?;
let game = Game::new(&config.game.path);
// Update voiceovers states
status_page.set_description(Some("Updating voiceovers info..."));
for package in &*self.widgets.voieover_components {
package.update_state(&config.game.path);
}
// Update game version
status_page.set_description(Some("Updating game info..."));
self.widgets.game_version.set_tooltip_text(None);
self.widgets.patch_version.set_tooltip_text(None);
match game.try_get_diff()? {
VersionDiff::Latest(version) => {
self.widgets.game_version.set_label(&version.to_string());
}
VersionDiff::Predownload { current, latest, .. } => {
self.widgets.game_version.set_label(&current.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(&current.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(&current.to_string());
self.widgets.game_version.set_css_classes(&["error"]);
self.widgets.game_version.set_tooltip_text(Some(&format!("Game is too outdated and can't be updated. Latest version: {latest}")));
}
VersionDiff::NotInstalled { .. } => {
self.widgets.game_version.set_label("not installed");
self.widgets.game_version.set_css_classes(&[]);
}
}
// Update patch version
status_page.set_description(Some("Updating patch info..."));
let patch = Patch::try_fetch(config.patch.servers, consts::PATCH_FETCHING_TIMEOUT)?;
match patch {
Patch::NotAvailable => {
self.widgets.patch_version.set_label("not available");
self.widgets.patch_version.set_css_classes(&["error"]);
self.widgets.patch_version.set_tooltip_text(Some("Patch is not available"));
}
Patch::Outdated { current, latest, .. } => {
self.widgets.patch_version.set_label(&format!("outdated ({})", current));
self.widgets.patch_version.set_css_classes(&["warning"]);
self.widgets.patch_version.set_tooltip_text(Some(&format!("Patch is outdated ({current} -> {latest})")));
}
Patch::Preparation { .. } => {
self.widgets.patch_version.set_label("preparation");
self.widgets.patch_version.set_css_classes(&["warning"]);
self.widgets.patch_version.set_tooltip_text(Some("Patch is in preparation state and will be available later"));
}
Patch::Testing { version, .. } => {
self.widgets.patch_version.set_label(&version.to_string());
self.widgets.patch_version.set_css_classes(&["warning"]);
self.widgets.patch_version.set_tooltip_text(Some("Patch is in testing phase"));
}
Patch::Available { version, .. } => {
self.widgets.patch_version.set_label(&version.to_string());
if let Ok(true) = patch.is_applied(&config.game.path) {
self.widgets.patch_version.set_css_classes(&["success"]);
}
else {
self.widgets.patch_version.set_css_classes(&["warning"]);
self.widgets.patch_version.set_tooltip_text(Some("Patch is not applied"));
}
}
}
// Update downloaded wine versions
self.update(Actions::UpdateWineComboRow).unwrap();
// Update downloaded DXVK versions
self.update(Actions::UpdateDxvkComboRow).unwrap();
Ok(())
}
}
impl Toast for App {
fn get_toast_widgets(&self) -> (adw::ApplicationWindow, adw::ToastOverlay) {
let app = (*self.app).take();
self.app.set(app.clone());
app.unwrap().get_toast_widgets()
}
}
unsafe impl Send for App {}
unsafe impl Sync for App {}

View 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");
}
}
}
}

View file

@ -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 {}

View file

@ -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))
}
}

View file

@ -1,7 +0,0 @@
pub mod toast;
pub mod download_component;
pub mod prelude {
pub use super::toast::*;
pub use super::download_component::*;
}

View file

@ -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);
}
}