Initial commit
This commit is contained in:
commit
cbda62e750
24 changed files with 6227 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
11
.idea/luncher.iml
generated
Normal file
11
.idea/luncher.iml
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/luncher.iml" filepath="$PROJECT_DIR$/.idea/luncher.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
5128
Cargo.lock
generated
Normal file
5128
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
32
Cargo.toml
Normal file
32
Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "luncher"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
egui = { version = "0.30.0", features = ["deadlock_detection", "persistence"] }
|
||||
eframe = { version = "0.30.0", features = ["persistence"] }
|
||||
ehttp = { version = "0.5.0", features = ["streaming"] }
|
||||
|
||||
egui_extras = { version = "0.30.0", features = ["image"] }
|
||||
egui-phosphor = { version = "0.8.0", features = ["fill"] }
|
||||
egui_flex = "0.2.0"
|
||||
|
||||
dark-light = "2.0.0"
|
||||
rfd = "0.15.2"
|
||||
|
||||
toml = "0.8.12"
|
||||
serde = "1.0.201"
|
||||
serde_derive = "1.0.201"
|
||||
serde_json = "1.0.135"
|
||||
|
||||
keyring = { version = "3.6.1", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||
|
||||
directories = "6.0.0"
|
||||
|
||||
blake3 = "1.5.5"
|
||||
|
||||
image = { version = "0.25.5", features = ["png"] }
|
||||
|
||||
[profile.release]
|
||||
strip="symbols"
|
176
LICENSE
Normal file
176
LICENSE
Normal file
|
@ -0,0 +1,176 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# osrs luncher
|
BIN
assets/Cooking_icon.png
Normal file
BIN
assets/Cooking_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/Steam_client_logo.png
Normal file
BIN
assets/Steam_client_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 175 KiB |
76
src/backend/config.rs
Normal file
76
src/backend/config.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use directories::ProjectDirs;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
pub static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
pub static CONFIG_FILE: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Paths {
|
||||
pub runelite_path: String,
|
||||
pub java_path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Versions {
|
||||
pub runelite_version: String,
|
||||
pub runelite_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub paths: Paths,
|
||||
pub versions: Versions,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Config {
|
||||
let mut runelite_path = CONFIG_DIR.get().unwrap().clone();
|
||||
runelite_path.push("runelite.jar");
|
||||
|
||||
let new_config = Config {
|
||||
paths: Paths { runelite_path: runelite_path.into_os_string().into_string().unwrap(), java_path: "java".to_string() },
|
||||
versions: Versions { runelite_version: "".to_string(), runelite_hash: "".to_string() },
|
||||
};
|
||||
|
||||
new_config.save_config();
|
||||
|
||||
new_config
|
||||
}
|
||||
|
||||
pub fn get_config() -> Config {
|
||||
let binding = ProjectDirs::from("sex", "gaycatgirl", "osrs-luncher").expect("Could not generate config path");
|
||||
let expected_config_dir = binding.config_dir();
|
||||
CONFIG_DIR.set(expected_config_dir.to_path_buf()).expect("Failed to set config dir");
|
||||
|
||||
let expected_config_file = expected_config_dir.join("config.toml");
|
||||
CONFIG_FILE.set(expected_config_file.to_path_buf()).expect("Failed to set config file");
|
||||
|
||||
// check to see if a config already exists at the expected path
|
||||
match fs::read_to_string(&expected_config_file) {
|
||||
Ok(config_contents) => {
|
||||
toml::from_str::<Config>(&config_contents).unwrap_or_else(|_| Config::new())
|
||||
}
|
||||
Err(error) => {
|
||||
match error.kind() {
|
||||
ErrorKind::NotFound => {
|
||||
// config doesn't exist, create
|
||||
Config::new()
|
||||
}
|
||||
_ => panic!("Could not read config file: {}", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_config(&self) {
|
||||
let file_path = CONFIG_FILE.get().unwrap().clone();
|
||||
|
||||
fs::create_dir_all(file_path.parent().unwrap()).expect("Could not create config directory");
|
||||
fs::write(file_path, toml::to_string_pretty(self).expect("Failed to generate config string")).expect("Could not write config file");
|
||||
}
|
||||
}
|
0
src/backend/hash.rs
Normal file
0
src/backend/hash.rs
Normal file
85
src/backend/http.rs
Normal file
85
src/backend/http.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::sync::mpsc::channel;
|
||||
use ehttp::Response;
|
||||
use ehttp::streaming::Part;
|
||||
|
||||
|
||||
pub struct HttpState {
|
||||
pub channel: Option<std::sync::mpsc::Receiver<HttpChannelChunk>>,
|
||||
pub request_in_progress: bool,
|
||||
pub total_size: f32,
|
||||
pub current_size: f32,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub enum HttpChannelChunk {
|
||||
Start(f32),
|
||||
Data(Vec<u8>),
|
||||
End,
|
||||
Oneshot(Response)
|
||||
}
|
||||
|
||||
|
||||
impl HttpState {
|
||||
pub fn new() -> HttpState {
|
||||
HttpState {
|
||||
channel: None,
|
||||
request_in_progress: false,
|
||||
total_size: 0.0,
|
||||
current_size: 0.0,
|
||||
data: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_request(&mut self, req: ehttp::Request) {
|
||||
if !self.request_in_progress {
|
||||
self.request_in_progress = true;
|
||||
|
||||
let (sender, receiver) = channel();
|
||||
self.channel = Some(receiver);
|
||||
|
||||
ehttp::streaming::fetch(req, move |res| {
|
||||
let part = match res {
|
||||
Ok(part) => part,
|
||||
Err(err) => {
|
||||
eprintln!("waaa waaa waaa {err}");
|
||||
return std::ops::ControlFlow::Break(());
|
||||
}
|
||||
};
|
||||
|
||||
match part {
|
||||
Part::Response(response) => {
|
||||
if response.ok {
|
||||
let total_size = response.headers.get("Content-Length").expect("Missing Content-Length").parse().expect("Content-Length wasn't a number");
|
||||
|
||||
sender.send(HttpChannelChunk::Start(total_size)).expect("Unable to send start chunk");
|
||||
|
||||
std::ops::ControlFlow::Continue(())
|
||||
} else {
|
||||
std::ops::ControlFlow::Break(())
|
||||
}
|
||||
}
|
||||
Part::Chunk(chunk) => {
|
||||
if chunk.is_empty() {
|
||||
sender.send(HttpChannelChunk::End).expect("Unable to send end chunk");
|
||||
} else {
|
||||
sender.send(HttpChannelChunk::Data(chunk)).expect("Unable to send data chunk");
|
||||
}
|
||||
|
||||
std::ops::ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_oneshot_request(&mut self, req: ehttp::Request) {
|
||||
self.request_in_progress = true;
|
||||
|
||||
let (sender, receiver) = channel();
|
||||
self.channel = Some(receiver);
|
||||
|
||||
ehttp::fetch(req, move |res| {
|
||||
sender.send(HttpChannelChunk::Oneshot(res.expect("Invalid response"))).expect("Failed to send oneshot response")
|
||||
})
|
||||
}
|
||||
}
|
0
src/backend/messages.rs
Normal file
0
src/backend/messages.rs
Normal file
5
src/backend/mod.rs
Normal file
5
src/backend/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod secrets;
|
||||
pub mod messages;
|
||||
pub mod http;
|
||||
pub mod config;
|
||||
mod hash;
|
48
src/backend/secrets.rs
Normal file
48
src/backend/secrets.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use keyring::Entry;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Account {
|
||||
pub session_id: String,
|
||||
pub character_id: String,
|
||||
pub character_name: String,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn get_secrets() -> Option<Self> {
|
||||
let session_id = Entry::new("osrs-luncher", "session-id").expect("Unable to get session_id entry");
|
||||
|
||||
if session_id.get_password().is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let character_id = Entry::new("osrs-luncher", "character-id").expect("Unable to get character_id entry");
|
||||
let character_name = Entry::new("osrs-luncher", "character-name").expect("Unable to get character_name entry");
|
||||
|
||||
Some(Account {
|
||||
session_id: session_id.get_password().expect("Unable to get session_id entry content"),
|
||||
character_id: character_id.get_password().expect("Unable to get character_id entry content"),
|
||||
character_name: character_name.get_password().expect("Unable to get character_name entry content"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_secrets(&self) -> bool {
|
||||
let session_id = Entry::new("osrs-luncher", "session-id").expect("Unable to get session_id entry");
|
||||
let character_id = Entry::new("osrs-luncher", "character-id").expect("Unable to get character_id entry");
|
||||
let character_name = Entry::new("osrs-luncher", "character-name").expect("Unable to get character_name entry");
|
||||
|
||||
session_id.set_password(self.session_id.as_str()).expect("Unable to set session_id");
|
||||
character_id.set_password(self.character_id.as_str()).expect("Unable to set character_id");
|
||||
character_name.set_password(self.character_name.as_str()).expect("Unable to set character_name");
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn delete_secrets(&self) -> bool {
|
||||
let session_id = Entry::new("osrs-luncher", "session-id").expect("Unable to get session_id entry").delete_credential().is_ok();
|
||||
let character_id = Entry::new("osrs-luncher", "character-id").expect("Unable to get character_id entry").delete_credential().is_ok();
|
||||
let character_name = Entry::new("osrs-luncher", "character-name").expect("Unable to get character_name entry").delete_credential().is_ok();
|
||||
|
||||
session_id | character_id | character_name
|
||||
}
|
||||
}
|
161
src/main.rs
Normal file
161
src/main.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use std::process::Child;
|
||||
use dark_light::Mode;
|
||||
use egui::{IconData, Theme, ViewportBuilder};
|
||||
use egui_extras::install_image_loaders;
|
||||
use egui_flex::{item, Flex, FlexAlign, FlexAlignContent};
|
||||
use image::GenericImageView;
|
||||
use backend::config::{Config, Paths, Versions};
|
||||
use crate::backend::http::HttpState;
|
||||
use crate::backend::secrets::Account;
|
||||
use crate::ui::landing::Landing;
|
||||
use crate::ui::{game_open, Pages};
|
||||
use crate::ui::launcher::Launcher;
|
||||
|
||||
mod ui;
|
||||
mod backend;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
let default_theme: Theme = match dark_light::detect() {
|
||||
Ok(Mode::Dark) => Theme::Dark,
|
||||
_ => Theme::Light
|
||||
};
|
||||
|
||||
let icon = image::load_from_memory(include_bytes!("../assets/Cooking_icon.png")).expect("Failed to load Cooking_icon").to_rgba8();
|
||||
|
||||
let viewport = egui::ViewportBuilder::default()
|
||||
.with_title("OSRS Luncher")
|
||||
.with_app_id("sex.gaycatgirl.luncher")
|
||||
.with_icon(IconData {
|
||||
rgba: icon.clone().into_raw(),
|
||||
width: icon.width(),
|
||||
height: icon.height(),
|
||||
})
|
||||
.with_fullscreen(true);
|
||||
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Fill);
|
||||
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"osrs luncher",
|
||||
options,
|
||||
Box::new(|ctx| {
|
||||
ctx.egui_ctx.set_fonts(fonts);
|
||||
ctx.egui_ctx.set_theme(default_theme);
|
||||
|
||||
install_image_loaders(&ctx.egui_ctx);
|
||||
|
||||
Ok(Box::<App>::default())
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
struct State {
|
||||
child: Option<Child>,
|
||||
game_open: bool,
|
||||
current_page: Pages,
|
||||
next_page: Option<Pages>,
|
||||
}
|
||||
|
||||
struct App {
|
||||
config: Config,
|
||||
account: Option<Account>,
|
||||
state: State,
|
||||
http_state: HttpState,
|
||||
working: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
App {
|
||||
config: Config { paths: Paths { runelite_path: "".to_string(), java_path: "".to_string() }, versions: Versions { runelite_version: "".to_string(), runelite_hash: "".to_string() } },
|
||||
account: None,
|
||||
state: State { child: None, game_open: false, current_page: Pages::Landing(None), next_page: None },
|
||||
http_state: HttpState::new(),
|
||||
working: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// If we've requested a page change, change to it
|
||||
if let Some(next_page) = self.state.next_page.take() {
|
||||
self.state.current_page = next_page;
|
||||
|
||||
self.state.next_page = None;
|
||||
}
|
||||
|
||||
// Look to see if the child process has exited yet and reset to the launcher if it has
|
||||
if let Some(child) = self.state.child.as_mut() {
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
self.state.game_open = false;
|
||||
self.state.child = None;
|
||||
|
||||
self.state.current_page = Pages::Launcher(Launcher::init());
|
||||
}
|
||||
};
|
||||
|
||||
// Make sure to show the game open page when its open
|
||||
if self.state.game_open {
|
||||
self.state.current_page = Pages::GameOpen;
|
||||
}
|
||||
|
||||
egui::TopBottomPanel::bottom("footer").show(ctx, |ui| {
|
||||
Flex::new().w_full().show(ui, |flex| {
|
||||
flex.add_ui(item(), |ui| {
|
||||
ui.label(format!("osrs luncher v{}", env!("CARGO_PKG_VERSION")));
|
||||
ui.separator();
|
||||
ui.hyperlink_to("see the source", "https://github.com/");
|
||||
});
|
||||
|
||||
flex.grow();
|
||||
|
||||
flex.add_ui(item(), |ui| {
|
||||
if self.working {
|
||||
ui.spinner();
|
||||
ui.label("working...");
|
||||
|
||||
self.working = false;
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, move |ui| {
|
||||
ui.heading("osrs luncher");
|
||||
|
||||
ui.separator();
|
||||
|
||||
match &mut self.state.current_page {
|
||||
Pages::Landing(landing) => {
|
||||
if let Some(landing) = landing.as_mut() {
|
||||
landing.update(&mut self.state.next_page, &mut self.http_state, &mut self.account, &mut self.config, &mut self.working, ctx, ui)
|
||||
} else {
|
||||
*landing = Some(Landing::init())
|
||||
}
|
||||
}
|
||||
Pages::Login(login) => {
|
||||
login.update(&mut self.state.next_page, &mut self.account, &mut self.working, ctx, ui)
|
||||
}
|
||||
Pages::Settings(settings) => {
|
||||
settings.update(&mut self.state.next_page, &mut self.config, ctx, ui)
|
||||
}
|
||||
Pages::Launcher(launcher) => {
|
||||
launcher.update(&mut self.state.next_page, &mut self.account, &mut self.config, &mut self.state.child, &mut self.state.game_open, ctx, ui);
|
||||
}
|
||||
Pages::GameOpen => {
|
||||
game_open::update(&mut self.state.child, ctx, ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
10
src/ui/game_open.rs
Normal file
10
src/ui/game_open.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use std::process::Child;
|
||||
use egui::{Context, Ui};
|
||||
|
||||
pub fn update(child: &mut Option<Child>, ctx: &Context, ui: &mut Ui) {
|
||||
ui.heading("Close OSRS to continue!");
|
||||
|
||||
if ui.button("Close").clicked() {
|
||||
child.as_mut().unwrap().kill().expect("Failed to kill");
|
||||
}
|
||||
}
|
199
src/ui/landing.rs
Normal file
199
src/ui/landing.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use std::fs;
|
||||
use std::path::Path;
|
||||
use egui::{Context, ProgressBar, Ui};
|
||||
use serde_json::{Map, Value};
|
||||
use crate::backend::config::{Config, CONFIG_DIR};
|
||||
use crate::backend::http::HttpChannelChunk;
|
||||
use crate::backend::secrets::Account;
|
||||
use crate::HttpState;
|
||||
use crate::ui::launcher::Launcher;
|
||||
use crate::ui::login::{Login, LoginType};
|
||||
use crate::ui::Pages;
|
||||
|
||||
pub struct Landing {
|
||||
stage: LandingStage,
|
||||
server_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LandingStage {
|
||||
Config,
|
||||
CheckUpdate,
|
||||
Verify,
|
||||
Download,
|
||||
Account,
|
||||
}
|
||||
|
||||
impl Landing {
|
||||
pub fn init() -> Self {
|
||||
Landing {
|
||||
stage: LandingStage::Config,
|
||||
server_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self,
|
||||
next_page: &mut Option<Pages>,
|
||||
http_state: &mut HttpState,
|
||||
account: &mut Option<Account>,
|
||||
config: &mut Config,
|
||||
working: &mut bool,
|
||||
ctx: &Context,
|
||||
ui: &mut Ui
|
||||
) {
|
||||
match self.stage {
|
||||
LandingStage::Config => {
|
||||
ui.label("loading config");
|
||||
*working = true;
|
||||
|
||||
*config = Config::get_config();
|
||||
|
||||
self.stage = LandingStage::CheckUpdate;
|
||||
}
|
||||
LandingStage::CheckUpdate => {
|
||||
ui.label("checking for updates");
|
||||
*working = true;
|
||||
|
||||
// Only do these checks if we're managing the runelite jar
|
||||
if Path::new(&config.paths.runelite_path).to_path_buf().parent().unwrap().eq(CONFIG_DIR.get().unwrap()) {
|
||||
if http_state.request_in_progress {
|
||||
if let Some(channel) = &http_state.channel {
|
||||
if let Ok(res) = channel.try_recv() {
|
||||
match res {
|
||||
HttpChannelChunk::Oneshot(res) => {
|
||||
let res_json: Map<String, Value> = serde_json::from_slice(&res.bytes).expect("Invalid json from server");
|
||||
|
||||
let server_version = res_json.get("tag_name").expect("Missing tag_name in server response").as_str().unwrap().to_string();
|
||||
|
||||
println!("Server version: {}", server_version);
|
||||
|
||||
self.server_version = Some(server_version.clone());
|
||||
|
||||
if config.versions.runelite_version.ne(&server_version) {
|
||||
self.stage = LandingStage::Download;
|
||||
} else {
|
||||
self.stage = LandingStage::Verify;
|
||||
}
|
||||
|
||||
// We're done, destroy channel and reset fields
|
||||
http_state.channel = None;
|
||||
http_state.data = vec![];
|
||||
http_state.total_size = 0.0;
|
||||
http_state.current_size = 0.0;
|
||||
http_state.request_in_progress = false;
|
||||
}
|
||||
_ => {
|
||||
panic!("Invalid!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
http_state.do_oneshot_request(ehttp::Request::get("https://api.github.com/repos/runelite/launcher/releases/latest"));
|
||||
}
|
||||
}
|
||||
}
|
||||
LandingStage::Verify => {
|
||||
ui.label("verifying runelite");
|
||||
*working = true;
|
||||
|
||||
self.stage = LandingStage::Account;
|
||||
|
||||
if !Path::new(&config.paths.runelite_path).exists() {
|
||||
self.stage = LandingStage::Download;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Only do these checks if we're managing the runelite jar
|
||||
if Path::new(&config.paths.runelite_path).to_path_buf().parent().unwrap().eq(CONFIG_DIR.get().unwrap()) {
|
||||
if config.versions.runelite_version.is_empty() || config.versions.runelite_hash.is_empty() {
|
||||
self.stage = LandingStage::Download;
|
||||
}
|
||||
|
||||
let jar = fs::read(&config.paths.runelite_path);
|
||||
match jar {
|
||||
Ok(jar) => {
|
||||
if blake3::hash(&jar).to_hex().to_string() != config.versions.runelite_hash {
|
||||
self.stage = LandingStage::Download;
|
||||
}
|
||||
}
|
||||
_=> {
|
||||
self.stage = LandingStage::Download;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LandingStage::Download => {
|
||||
ui.label("downloading runelite");
|
||||
*working = true;
|
||||
|
||||
let update_to = self.server_version.clone().expect("Asked to download without version!");
|
||||
|
||||
if http_state.request_in_progress {
|
||||
let progress = http_state.current_size / http_state.total_size;
|
||||
|
||||
ui.add(ProgressBar::new(progress));
|
||||
ui.label(format!("{:.1}%", (progress * 100.0).max(0.0)));
|
||||
|
||||
if let Some(channel) = &http_state.channel {
|
||||
while let Ok(res) = channel.try_recv(){
|
||||
match res {
|
||||
HttpChannelChunk::Start(total) => {
|
||||
http_state.total_size = total;
|
||||
}
|
||||
HttpChannelChunk::Data(data) => {
|
||||
http_state.current_size += data.len() as f32;
|
||||
http_state.data.extend(data);
|
||||
}
|
||||
HttpChannelChunk::End => {
|
||||
// We're done, destroy channel, write buffer, and reset fields
|
||||
http_state.channel = None;
|
||||
|
||||
// this isn't the best behaviour (we're writing a good amount of data sync'ed) but its easy and fast so who cares
|
||||
fs::write(&config.paths.runelite_path, &http_state.data).expect("Failed to write runelite jar");
|
||||
|
||||
// same bad behaviour as above
|
||||
config.versions.runelite_hash = blake3::hash(&http_state.data).to_hex().to_string();
|
||||
config.versions.runelite_version = update_to;
|
||||
config.save_config();
|
||||
|
||||
http_state.data = vec![];
|
||||
|
||||
http_state.total_size = 0.0;
|
||||
http_state.current_size = 0.0;
|
||||
|
||||
http_state.request_in_progress = false;
|
||||
|
||||
// TODO: verify or smth
|
||||
|
||||
self.stage = LandingStage::Account;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
panic!("Unexpected http channel chunk");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
http_state.do_request(ehttp::Request::get(format!("https://github.com/runelite/launcher/releases/download/{}/RuneLite.jar", update_to)));
|
||||
}
|
||||
}
|
||||
LandingStage::Account => {
|
||||
ui.label("loading account");
|
||||
*working = true;
|
||||
|
||||
*account = Account::get_secrets();
|
||||
|
||||
if account.is_none() {
|
||||
*next_page = Some(Pages::Login(Login::init(LoginType::NoAccount)));
|
||||
} else {
|
||||
// TODO: verification that confirms the creds are valid
|
||||
*next_page = Some(Pages::Launcher(Launcher::init()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
src/ui/launcher.rs
Normal file
85
src/ui/launcher.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::process::{Child, Command};
|
||||
use egui::{Color32, Context, RichText, Ui};
|
||||
use egui_flex::{item, Flex, FlexAlign, FlexDirection};
|
||||
use egui_phosphor::fill;
|
||||
use crate::backend::config::Config;
|
||||
use crate::backend::secrets::Account;
|
||||
use crate::ui::login::{Login, LoginType};
|
||||
use crate::ui::Pages;
|
||||
use crate::ui::settings::Settings;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Launcher {
|
||||
|
||||
}
|
||||
|
||||
impl Launcher {
|
||||
pub fn init() -> Launcher {
|
||||
Launcher {}
|
||||
}
|
||||
|
||||
pub fn update(&mut self,
|
||||
next_page: &mut Option<Pages>,
|
||||
account: &mut Option<Account>,
|
||||
config: &mut Config,
|
||||
child: &mut Option<Child>,
|
||||
game_open: &mut bool,
|
||||
_ctx: &Context,
|
||||
ui: &mut Ui
|
||||
) {
|
||||
let checked_account = if let Some(account) = account {
|
||||
account.clone()
|
||||
} else {
|
||||
*next_page = Some(Pages::Login(Login::init(LoginType::NoAccount)));
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
Flex::new().w_full().h_full().direction(FlexDirection::Vertical).show(ui, |flex| {
|
||||
flex.add_flex(item(), Flex::horizontal().w_full(), |flex| {
|
||||
flex.add_ui(item(), |ui| {
|
||||
ui.heading(format!("Hello, {}!", checked_account.character_name));
|
||||
});
|
||||
|
||||
flex.grow();
|
||||
|
||||
flex.add_ui(item(), |ui| {
|
||||
if ui.button(RichText::new(fill::GEAR).size(20.0)).clicked() {
|
||||
*next_page = Some(Pages::Settings(Settings::init()));
|
||||
}
|
||||
|
||||
if ui.button(RichText::new(fill::SIGN_OUT).color(Color32::RED).size(20.0)).clicked() {
|
||||
checked_account.delete_secrets();
|
||||
|
||||
*account = None;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
flex.grow();
|
||||
|
||||
flex.add_ui(item().align_self(FlexAlign::Center), |ui| {
|
||||
ui.add(egui::Image::new(egui::include_image!("../../assets/Steam_client_logo.png")).max_height(250f32));
|
||||
});
|
||||
|
||||
flex.add_ui(item().align_self(FlexAlign::Center), |ui| {
|
||||
if ui.button(RichText::new("Play").size(30.0)).clicked() {
|
||||
*child = Some(Command::new(config.paths.java_path.clone())
|
||||
.arg("-jar")
|
||||
.arg(config.paths.runelite_path.clone())
|
||||
.arg("--launch-mode")
|
||||
.arg("REFLECT")
|
||||
.env("JX_SESSION_ID", checked_account.session_id.clone())
|
||||
.env("JX_CHARACTER_ID", checked_account.character_id.clone())
|
||||
.env("JX_DISPLAY_NAME", checked_account.character_name.clone())
|
||||
.spawn()
|
||||
.expect("Failed to start RuneLite!"));
|
||||
|
||||
*game_open = true;
|
||||
}
|
||||
});
|
||||
|
||||
flex.grow();
|
||||
});
|
||||
}
|
||||
}
|
114
src/ui/login.rs
Normal file
114
src/ui/login.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use std::cmp::PartialEq;
|
||||
use egui::{Context, Ui};
|
||||
use crate::backend::secrets::Account;
|
||||
use crate::ui::launcher::Launcher;
|
||||
use crate::ui::Pages;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Login {
|
||||
login_type: LoginType,
|
||||
login_error_type: Option<LoginErrorType>,
|
||||
session_id: String,
|
||||
character_name: String,
|
||||
character_id: String,
|
||||
save_credentials: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LoginType {
|
||||
NoAccount,
|
||||
ExpiredCredentials,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum LoginErrorType {
|
||||
LoggingIn,
|
||||
MissingFields,
|
||||
FailedToSave
|
||||
}
|
||||
|
||||
impl Login {
|
||||
pub fn init(login_type: LoginType) -> Login {
|
||||
Login { login_type, login_error_type: None, session_id: "".to_string(), character_name: "".to_string(), character_id: "".to_string(), save_credentials: false }
|
||||
}
|
||||
|
||||
pub fn update(&mut self,
|
||||
next_page: &mut Option<Pages>,
|
||||
account: &mut Option<Account>,
|
||||
working: &mut bool,
|
||||
_ctx: &Context,
|
||||
ui: &mut Ui
|
||||
) {
|
||||
ui.heading("Login");
|
||||
|
||||
match self.login_type {
|
||||
LoginType::NoAccount => {
|
||||
ui.label("No account");
|
||||
}
|
||||
LoginType::ExpiredCredentials => {
|
||||
ui.label("Expired credentials");
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Character Name: ");
|
||||
|
||||
ui.add(egui::TextEdit::singleline(&mut self.character_name));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Character ID: ");
|
||||
ui.add(egui::TextEdit::singleline(&mut self.character_id).password(true));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Session ID: ");
|
||||
ui.add(egui::TextEdit::singleline(&mut self.session_id).password(true));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Login").clicked() {
|
||||
self.login_error_type = None;
|
||||
|
||||
if self.character_name.is_empty() | self.character_id.is_empty() | self.session_id.is_empty() {
|
||||
self.login_error_type = Some(LoginErrorType::MissingFields);
|
||||
} else {
|
||||
self.login_error_type = Some(LoginErrorType::LoggingIn);
|
||||
|
||||
let new_account = Account {
|
||||
session_id: self.session_id.clone(),
|
||||
character_id: self.character_id.clone(),
|
||||
character_name: self.character_name.clone(),
|
||||
};
|
||||
|
||||
*account = Some(new_account.clone());
|
||||
|
||||
if self.save_credentials && !new_account.insert_secrets() {
|
||||
self.login_error_type = Some(LoginErrorType::FailedToSave);
|
||||
}
|
||||
|
||||
if self.login_error_type.clone().is_some_and(|x| x.eq(&LoginErrorType::LoggingIn)) {
|
||||
*next_page = Some(Pages::Launcher(Launcher::init()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(error_type) = self.login_error_type.clone() {
|
||||
match error_type {
|
||||
LoginErrorType::MissingFields => {
|
||||
ui.label("Missing fields");
|
||||
}
|
||||
LoginErrorType::LoggingIn => {
|
||||
ui.label("Logging in...");
|
||||
*working = true;
|
||||
}
|
||||
LoginErrorType::FailedToSave => {
|
||||
ui.label("Failed to save credentials.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.save_credentials, "Save credentials?");
|
||||
}
|
||||
}
|
18
src/ui/mod.rs
Normal file
18
src/ui/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use crate::ui::landing::Landing;
|
||||
use crate::ui::launcher::Launcher;
|
||||
use crate::ui::login::Login;
|
||||
use crate::ui::settings::Settings;
|
||||
|
||||
pub mod landing;
|
||||
pub mod game_open;
|
||||
pub mod login;
|
||||
pub mod launcher;
|
||||
pub mod settings;
|
||||
|
||||
pub enum Pages {
|
||||
Landing(Option<Landing>),
|
||||
Login(Login),
|
||||
Settings(Settings),
|
||||
Launcher(Launcher),
|
||||
GameOpen,
|
||||
}
|
55
src/ui/settings.rs
Normal file
55
src/ui/settings.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use egui::{Context, Ui};
|
||||
use egui_phosphor::fill;
|
||||
use rfd::FileDialog;
|
||||
use crate::backend::config::Config;
|
||||
use crate::ui::launcher::Launcher;
|
||||
use crate::ui::Pages;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn init() -> Settings {
|
||||
Settings {}
|
||||
}
|
||||
|
||||
pub fn update(&mut self,
|
||||
next_page: &mut Option<Pages>,
|
||||
config: &mut Config,
|
||||
_ctx: &Context,
|
||||
ui: &mut Ui
|
||||
) {
|
||||
ui.heading("Settings");
|
||||
|
||||
if ui.button(format!("{} Open Data Folder", fill::FOLDER)).clicked() {
|
||||
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("RuneLite path: ");
|
||||
ui.add(egui::TextEdit::singleline(&mut config.paths.runelite_path));
|
||||
|
||||
if ui.button(format!("{} Select", fill::FOLDER)).clicked() {
|
||||
// TODO: switch this to use a channel and another thread
|
||||
config.paths.runelite_path = FileDialog::new().pick_file().unwrap_or_default().into_os_string().into_string().unwrap_or_default();
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Java path: ");
|
||||
ui.add(egui::TextEdit::singleline(&mut config.paths.java_path));
|
||||
|
||||
if ui.button(format!("{} Select", fill::FOLDER)).clicked() {
|
||||
// TODO: switch this to use a channel and another thread
|
||||
config.paths.java_path = FileDialog::new().pick_file().unwrap_or_default().into_os_string().into_string().unwrap_or_default();
|
||||
}
|
||||
});
|
||||
|
||||
if ui.button("Save and Go Back").clicked() {
|
||||
config.save_config();
|
||||
|
||||
*next_page = Some(Pages::Launcher(Launcher::init()))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue