mirror of
https://github.com/Grasscutters/Cultivation.git
synced 2025-12-13 15:44:35 +01:00
Merge pull request #153 from NotThorny/ModBrowser-Improved
Improve Mod Browser
This commit is contained in:
@@ -107,9 +107,9 @@ async fn parse_args(inp: &Vec<String>) -> Result<Args, ArgsError> {
|
|||||||
|
|
||||||
if game_path.is_some() {
|
if game_path.is_some() {
|
||||||
if args.value_of("non-elevated-game")? {
|
if args.value_of("non-elevated-game")? {
|
||||||
system_helpers::run_un_elevated(game_path.unwrap().to_string(), Some(game_args))
|
system_helpers::run_un_elevated(game_path.unwrap(), Some(game_args))
|
||||||
} else {
|
} else {
|
||||||
system_helpers::run_program(game_path.unwrap().to_string(), Some(game_args))
|
system_helpers::run_program(game_path.unwrap(), Some(game_args))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,6 +308,7 @@ fn is_grasscutter_running() -> bool {
|
|||||||
!proc.is_empty()
|
!proc.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn restart_grasscutter(window: tauri::Window) -> bool {
|
fn restart_grasscutter(window: tauri::Window) -> bool {
|
||||||
let pid: usize = *GC_PID.lock().unwrap();
|
let pid: usize = *GC_PID.lock().unwrap();
|
||||||
@@ -338,6 +339,11 @@ fn restart_grasscutter(window: tauri::Window) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tauri::command]
|
||||||
|
fn restart_grasscutter(window: tauri::Window) {}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
|
fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
|
||||||
let grasscutter_name = process.clone();
|
let grasscutter_name = process.clone();
|
||||||
@@ -398,6 +404,12 @@ fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tauri::command]
|
||||||
|
fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
|
||||||
|
let gc_pid = Pid::from(696969);
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn connect(port: u16, certificate_path: String) {
|
async fn connect(port: u16, certificate_path: String) {
|
||||||
// Log message to console.
|
// Log message to console.
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ pub async fn unpatch_game() -> bool {
|
|||||||
pub async fn get_game_rsa_path() -> Option<String> {
|
pub async fn get_game_rsa_path() -> Option<String> {
|
||||||
let config = config::get_config();
|
let config = config::get_config();
|
||||||
|
|
||||||
if config.game_install_path.is_none() {
|
config.game_install_path.as_ref()?;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut game_folder = PathBuf::from(config.game_install_path.unwrap());
|
let mut game_folder = PathBuf::from(config.game_install_path.unwrap());
|
||||||
game_folder.pop();
|
game_folder.pop();
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ pub async fn get_latest_release() -> Release {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let text = response.text().await.unwrap();
|
let text = response.text().await.unwrap();
|
||||||
|
|
||||||
println!("Response: {}", text);
|
// This includes ip when github rate limits you, so avoid it for now to avoid leaks through screenshots
|
||||||
|
//println!("Response: {}", text);
|
||||||
|
|
||||||
// Parse "tag_name" from JSON
|
// Parse "tag_name" from JSON
|
||||||
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use duct::cmd;
|
use duct::cmd;
|
||||||
use ini::Ini;
|
use ini::Ini;
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use windows_service::service::{ServiceAccess, ServiceState::Stopped};
|
use std::ffi::OsStr;
|
||||||
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use registry::{Data, Hive, Security};
|
use {
|
||||||
|
registry::{Data, Hive, Security},
|
||||||
|
windows_service::service::{ServiceAccess, ServiceState::Stopped},
|
||||||
|
windows_service::service_manager::{ServiceManager, ServiceManagerAccess},
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn run_program(path: String, args: Option<String>) {
|
pub fn run_program(path: String, args: Option<String>) {
|
||||||
@@ -230,7 +232,7 @@ pub fn service_status(service: String) -> bool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let status_result = my_service.query_status();
|
let status_result = my_service.query_status();
|
||||||
if status_result.is_ok() {
|
if let Ok(..) = status_result {
|
||||||
let status = status_result.unwrap();
|
let status = status_result.unwrap();
|
||||||
println!("{} service status: {:?}", service, status.current_state);
|
println!("{} service status: {:?}", service, status.current_state);
|
||||||
if status.current_state == Stopped {
|
if status.current_state == Stopped {
|
||||||
@@ -262,6 +264,12 @@ pub fn start_service(service: String) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn start_service(service: String) {
|
||||||
|
let started = OsStr::new("Started service!");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stop_service(service: String) -> bool {
|
pub fn stop_service(service: String) -> bool {
|
||||||
@@ -281,6 +289,10 @@ pub fn stop_service(service: String) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn stop_service(service: String) {}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn wipe_registry(_exec_name: String) {}
|
pub fn wipe_registry(_exec_name: String) {}
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
|
use http::header;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
|
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
|
||||||
|
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||||
|
let mut headers = header::HeaderMap::new();
|
||||||
|
headers.insert(USER_AGENT, header::HeaderValue::from_static("cultivation"));
|
||||||
|
headers.insert(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("application/json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder().default_headers(headers);
|
||||||
|
client.build().unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
pub(crate) async fn query(site: &str) -> String {
|
pub(crate) async fn query(site: &str) -> String {
|
||||||
let client = reqwest::Client::new();
|
CLIENT
|
||||||
|
|
||||||
let response = client
|
|
||||||
.get(site)
|
.get(site)
|
||||||
.header(USER_AGENT, "cultivation")
|
|
||||||
.header(CONTENT_TYPE, "application/json")
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.ok();
|
.expect("Failed to get web response")
|
||||||
|
.text()
|
||||||
if response.is_some() {
|
.await
|
||||||
response.unwrap().text().await.unwrap()
|
.unwrap()
|
||||||
} else {
|
|
||||||
false.to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Cultivation",
|
"productName": "Cultivation",
|
||||||
"version": "1.0.26"
|
"version": "1.0.27"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.Mods {
|
.Mods {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
height: 90%;
|
height: 80%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import Back from '../resources/icons/back.svg'
|
|||||||
import Menu from './components/menu/Menu'
|
import Menu from './components/menu/Menu'
|
||||||
import BigButton from './components/common/BigButton'
|
import BigButton from './components/common/BigButton'
|
||||||
import Tr from '../utils/language'
|
import Tr from '../utils/language'
|
||||||
|
import { ModPages } from './components/mods/ModPages'
|
||||||
|
import TextInput from './components/common/TextInput'
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
downloadHandler: DownloadHandler
|
downloadHandler: DownloadHandler
|
||||||
@@ -23,8 +25,21 @@ interface IState {
|
|||||||
isDownloading: boolean
|
isDownloading: boolean
|
||||||
category: string
|
category: string
|
||||||
downloadList: { name: string; url: string; mod: ModData }[] | null
|
downloadList: { name: string; url: string; mod: ModData }[] | null
|
||||||
|
page: number
|
||||||
|
search: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{
|
||||||
|
name: -1,
|
||||||
|
title: '<',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 1,
|
||||||
|
title: '>',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
{
|
{
|
||||||
name: 'ripe',
|
name: 'ripe',
|
||||||
@@ -46,17 +61,22 @@ const headers = [
|
|||||||
* @TODO Categorizaiton/sorting (by likes, views, etc)
|
* @TODO Categorizaiton/sorting (by likes, views, etc)
|
||||||
*/
|
*/
|
||||||
export class Mods extends React.Component<IProps, IState> {
|
export class Mods extends React.Component<IProps, IState> {
|
||||||
|
timeout: number
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.timeout = 0
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isDownloading: false,
|
isDownloading: false,
|
||||||
category: '',
|
category: '',
|
||||||
downloadList: null,
|
downloadList: null,
|
||||||
|
page: 1,
|
||||||
|
search: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setCategory = this.setCategory.bind(this)
|
this.setCategory = this.setCategory.bind(this)
|
||||||
this.addDownload = this.addDownload.bind(this)
|
this.addDownload = this.addDownload.bind(this)
|
||||||
|
this.setPage = this.setPage.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDownload(mod: ModData) {
|
async addDownload(mod: ModData) {
|
||||||
@@ -111,6 +131,29 @@ export class Mods extends React.Component<IProps, IState> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setPage(value: number) {
|
||||||
|
const current = this.state.page
|
||||||
|
if (current + value == 0) return
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
page: current + value,
|
||||||
|
},
|
||||||
|
this.forceUpdate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSearch(text: string) {
|
||||||
|
if (this.timeout) clearTimeout(this.timeout)
|
||||||
|
this.timeout = window.setTimeout(() => {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
search: text,
|
||||||
|
},
|
||||||
|
this.forceUpdate
|
||||||
|
)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="Mods">
|
<div className="Mods">
|
||||||
@@ -162,7 +205,30 @@ export class Mods extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
<ModHeader onChange={this.setCategory} headers={headers} defaultHeader={'ripe'} />
|
<ModHeader onChange={this.setCategory} headers={headers} defaultHeader={'ripe'} />
|
||||||
|
|
||||||
<ModList key={this.state.category} mode={this.state.category} addDownload={this.addDownload} />
|
{this.state.category != 'installed' && (
|
||||||
|
<>
|
||||||
|
<div className="ModPagesPage">
|
||||||
|
<TextInput
|
||||||
|
id="search"
|
||||||
|
key="search"
|
||||||
|
placeholder={this.state.page.toString()}
|
||||||
|
onChange={(text: string) => {
|
||||||
|
this.setSearch(text)
|
||||||
|
}}
|
||||||
|
initalValue={''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ModPages onClick={this.setPage} headers={pages} defaultHeader={this.state.page} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ModList
|
||||||
|
key={`${this.state.category}_${this.state.page}_${this.state.search}`}
|
||||||
|
mode={this.state.category}
|
||||||
|
addDownload={this.addDownload}
|
||||||
|
page={this.state.page}
|
||||||
|
search={this.state.search}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { getConfigOption } from '../../../utils/configuration'
|
import { getConfigOption } from '../../../utils/configuration'
|
||||||
import { getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana'
|
import { getAllMods, getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana'
|
||||||
import { LoadingCircle } from './LoadingCircle'
|
import { LoadingCircle } from './LoadingCircle'
|
||||||
|
|
||||||
import './ModList.css'
|
import './ModList.css'
|
||||||
@@ -8,6 +8,8 @@ import { ModTile } from './ModTile'
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mode: string
|
mode: string
|
||||||
|
page: number
|
||||||
|
search: string
|
||||||
addDownload: (mod: ModData) => void
|
addDownload: (mod: ModData) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +64,17 @@ export class ModList extends React.Component<IProps, IState> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mods = await getMods(this.props.mode)
|
let mods: ModData[]
|
||||||
|
|
||||||
|
if (!(this.props.search == '')) {
|
||||||
|
// idk the api so just filter all mods to search
|
||||||
|
mods = (await getAllMods(this.props.mode)).filter((mod) =>
|
||||||
|
mod.name.toLowerCase().includes(this.props.search.toLowerCase())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
mods = await getMods(this.props.mode, this.props.page)
|
||||||
|
}
|
||||||
|
|
||||||
const horny = await getConfigOption('horny_mode')
|
const horny = await getConfigOption('horny_mode')
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
66
src/ui/components/mods/ModPages.css
Normal file
66
src/ui/components/mods/ModPages.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
.ModPages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(77, 77, 77, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModPagesTitle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModPagesTitle:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModPagesTitle.selected {
|
||||||
|
border-bottom: 0px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModPagesPage {
|
||||||
|
position: absolute;
|
||||||
|
justify-self: center;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: 10px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
padding: -5px;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModPagesPage input {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 18px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(77, 77, 77, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ModPagesPage .TextInputWrapper {
|
||||||
|
background: rgba(77, 77, 77, 0.6);
|
||||||
|
z-index: -1;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
54
src/ui/components/mods/ModPages.tsx
Normal file
54
src/ui/components/mods/ModPages.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import './ModPages.css'
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
headers: {
|
||||||
|
title: string
|
||||||
|
name: number
|
||||||
|
}[]
|
||||||
|
onClick: (value: number) => void
|
||||||
|
defaultHeader: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
selected: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModPages extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selected: this.props.defaultHeader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected(value: number) {
|
||||||
|
const current = this.state.selected
|
||||||
|
if (current + value == 0) return
|
||||||
|
this.setState({
|
||||||
|
selected: current + value,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.props.onClick(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="ModPages">
|
||||||
|
{this.props.headers.map((header, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`ModPagesTitle ${this.state.selected === header.name ? 'selected' : ''}`}
|
||||||
|
onClick={() => this.setSelected(header.name)}
|
||||||
|
>
|
||||||
|
{header.title}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ export default class NewsSection extends React.Component<IProps, IState> {
|
|||||||
case 'latest_version':
|
case 'latest_version':
|
||||||
news = (
|
news = (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Latest version: Grasscutter 1.4.6 - Cultivation 1.0.10</td>
|
<td>Latest version: Grasscutter 1.4.8 - Cultivation 1.0.27</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -117,7 +117,22 @@ interface ModDownload {
|
|||||||
containsExe: boolean
|
containsExe: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMods(mode: string) {
|
export async function getMods(mode: string, page: number) {
|
||||||
|
let modList: GamebananaResponse[] = []
|
||||||
|
|
||||||
|
const resp = JSON.parse(
|
||||||
|
await invoke('list_submissions', {
|
||||||
|
mode,
|
||||||
|
page: '' + page,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
modList = [...modList, ...resp]
|
||||||
|
|
||||||
|
return formatGamebananaData(modList)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllMods(mode: string) {
|
||||||
let modList: GamebananaResponse[] = []
|
let modList: GamebananaResponse[] = []
|
||||||
let hadMods = true
|
let hadMods = true
|
||||||
let page = 1
|
let page = 1
|
||||||
@@ -134,13 +149,8 @@ export async function getMods(mode: string) {
|
|||||||
|
|
||||||
modList = [...modList, ...resp]
|
modList = [...modList, ...resp]
|
||||||
page++
|
page++
|
||||||
|
|
||||||
console.log(page)
|
|
||||||
console.log(resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(modList)
|
|
||||||
|
|
||||||
return formatGamebananaData(modList)
|
return formatGamebananaData(modList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user