Compare commits

...

9 Commits
1.0.0 ... main

Author SHA1 Message Date
e38f1669dc update Dockerfile 2025-01-15 10:27:43 +08:00
479ad3acd4 feat: 日志输出IP 2025-01-14 20:50:31 +08:00
11eaaa43d3 feat: 支持多语言 2025-01-13 23:33:03 +08:00
a5fb3cc100 feat: 调整服务器时间优化 2025-01-09 01:19:55 +08:00
621b554711 feat: 支持兑换码自定数量 2025-01-07 18:27:22 +08:00
ee2e8dd1c6 feat: 添加日志功能 2025-01-06 23:13:20 +08:00
5a0f888000 本地化翻译 2025-01-05 22:33:17 +08:00
1b6fb5fa81 fix: 脚本参数异常 2025-01-05 19:20:49 +08:00
d58a0d26cf 修改asset_index位置 2025-01-05 17:14:58 +08:00
29 changed files with 676 additions and 4919 deletions

View File

@ -31,3 +31,5 @@ serde = { version = "1.0.215", features = ["derive"] }
futures = "0.3.31"
mime_guess = "2.0.5"
actix-files = "0.6.6"
simplelog = "0.12.2"
log = "0.4.22"

View File

@ -1,18 +1,24 @@
# 打包命令
# docker buildx build -t lovelive_sif2:1.0.0 --platform=linux/amd64 .
# docker buildx build -t zhushenwudi/lovelive-sif2:1.0.2 -t zhushenwudi/lovelive-sif2:latest --platform=linux/amd64 .
# 使用适用于 Linux x86 的基础映像
# 保存镜像
# docker save -o lovelive_sif2.tar lovelive_sif2:1.0.0
# docker save -o lovelive-sif2.tar zhushenwudi/lovelive-sif2:1.0.2
FROM docker.io/library/debian:latest AS builder
# First - build
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
RUN apt update && apt install -y curl libssl-dev perl git gcc make
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash
RUN curl -fsSL https://deb.nodesource.com/setup_23.x | bash
RUN apt update && apt install -y nodejs
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
ENV TZ=Asia/Shanghai
RUN ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && dpkg-reconfigure -f noninteractive tzdata
RUN rm -rf /etc/localtime
RUN ln -sv /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /ew/
COPY ./ ./

View File

@ -1,34 +1,42 @@
version: '3'
services:
lovelive_sif2:
image: lovelive_sif2:latest
container_name: lovelive_sif2
image: zhushenwudi/lovelive-sif2:1.0.0
container_name: sif2
environment:
PORT: 8080
ASSET_PORT: 8000
DIRECTORY: /data/
ASSET_DIRECTORY: /assets/
# Server URL是否存在SSL/TSL加密协议
HTTPS: false
# 是否允许内部启动资源服务
ENABLE_ASSET_SERVER: false
# <数据迁移> SIF1服务器地址
#NPPS4_ADDRESS: "http://127.0.0.1:51376"
MAXTIME: 1717045200 # A day before global EOS
HIDDEN: false # Will disable the webui
DISABLE_IMPORTS: false # Will disable account imports
DISABLE_EXPORTS: false # Will disable account exports
# 服务器默认时间,建议设置到开服时间,最大设置为关服时间
MAXTIME: 1736076211
# 是否隐藏WebUI
HIDDEN: false
# 是否禁用账号导入
DISABLE_IMPORTS: false
# 是否禁用账号导出
DISABLE_EXPORTS: false
# Everything below is for the "Help" page
# 以下内容出现在Help界面
# 未签名的apk下载地址
ANDROID_JAPAN: "http://fir.zhushenwudi.top/n2tvzrhl"
#ANDROID_GLOBAL: "link.to/patched/android/global.apk"
#ANDROID_JAPAN: "link.to/patched/android/japan.apk"
# 脱壳的ipa下载地址
#IOS_JAPAN: "link.to/ios/jp.ipa"
#IOS_GLOBAL: "link.to/ios/global.ipa"
#IOS_JAPAN: "link.to/ios/japan.ipa"
#ASSET_URL: "link.to/client/assets/"
ports:
# 只修改左侧本地端口
# Server URL 端口
- "8080:8080"
# Assets URL 端口
- "8000:8000"
volumes:
# 只修改左侧本地路径
# 数据库存放路径(建议添加)
- ./data:/data
# 资源文件存放路径搭建asset server需要指定本地资源路径
# 资源文件存放路径搭建assets server需要指定本地资源路径
- ./assets:/assets
restart: unless-stopped

View File

@ -4,7 +4,7 @@ port="${PORT:-8080}"
directory="${DIRECTORY:-/data/}"
enable_asset_server=$([ "$ENABLE_ASSET_SERVER" = "true" ] && echo "--enable_asset_server" || echo "")
enable_asset_server=$([ "$ENABLE_ASSET_SERVER" = "true" ] && echo "--enable-asset-server" || echo "")
asset_port="${ASSET_PORT:-8000}"
@ -33,24 +33,24 @@ asset_android_en=$([ "$EN_ANDROID_ASSET_HASH" != "" ] && echo "--en-android-asse
asset_ios_en=$([ "$EN_IOS_ASSET_HASH" != "" ] && echo "--en-ios-asset-hash $EN_IOS_ASSET_HASH" || echo "")
/root/ew/ew \
--path "$directory" \
--port "$port" \
"$enable_asset_server" \
--asset_path "$assets_directory" \
--asset_port "$asset_port" \
--npps4 "$npps4" \
"$asset_android_jp" \
"$asset_ios_jp" \
"$asset_android_en" \
"$asset_ios_en" \
"$exports" \
"$imports" \
"$purge" \
"$hidden" \
"$https" \
--path $directory \
--port $port \
$enable_asset_server \
--asset-path $assets_directory \
--asset-port $asset_port \
--npps4 $npps4 \
$asset_android_jp \
$asset_ios_jp \
$asset_android_en \
$asset_ios_en \
$exports \
$imports \
$purge \
$hidden \
$https \
--global-android "$ANDROID_GLOBAL" \
--japan-android "$ANDROID_JAPAN" \
--global-ios "$IOS_GLOBAL" \
--japan-ios "$IOS_JAPAN" \
--assets-url "$ASSET_URL" \
--max-time "$maxTime"
--max-time $maxTime

View File

@ -1,31 +1,30 @@
# EW
(stands for ew why)
A (mostly functioning) server for Love Live! School idol festival 2 MIRACLE LIVE!
一款基本能正常运行的《Love Live! 学园偶像祭2 MIRACLE LIVE!》服务器程序
## Building
## 构建
### Linux
#### Requirements
- [perl](https://www.perl.org/get.html) (This is normally pre-installed)
#### 环境要求
- [perl](https://www.perl.org/get.html) (这通常是预装的)
- [rust](https://www.rust-lang.org/tools/install)
- [npm](https://www.npmjs.com/)
- The [libssl-dev](https://packages.debian.org/buster/libssl-dev) package. This will vary across distros.
- [libssl-dev](https://packages.debian.org/buster/libssl-dev) (这在不同的发行版中会有所不同)
`apt install -y npm libssl-dev perl`
### Windows
#### Requirements
#### 环境要求
- [Strawberry Perl](https://strawberryperl.com/)
- [rust](https://www.rust-lang.org/tools/install)
### Packaging/Running
### 打包/运行
**Build npm:**
**编译WebUI**
`cd webui && npm install && npm run build`
**Build Rust:**
**编译Rust**
Debug: `cargo run`
Release: `cargo build --release`
Release: `cargo build --release --jobs=16`

67
src/logger.rs Normal file
View File

@ -0,0 +1,67 @@
use log::{debug, error, info, warn, LevelFilter};
use simplelog::*;
use std::fs::{create_dir_all, File, OpenOptions};
use std::path::Path;
const MAX_LOG_SIZE: u64 = 50 * 1024 * 1024;
const LOG_FILE_PATH: &str = "data/rust.log";
pub fn init_logger() {
let mut builder = ConfigBuilder::new();
builder.set_time_offset_to_local().unwrap();
let config = builder.build();
// 获取文件的目录路径
let dir = Path::new(LOG_FILE_PATH).parent().unwrap();
// 检查并创建目录
if !dir.exists() {
create_dir_all(dir).unwrap();
}
let file = OpenOptions::new()
.write(true)
.append(true)
.create(true)
.open(LOG_FILE_PATH).unwrap();
CombinedLogger::init(
vec![
TermLogger::new(LevelFilter::Info, config.clone(), TerminalMode::Mixed, ColorChoice::Auto),
WriteLogger::new(LevelFilter::Info, config.clone(), file),
]
).unwrap();
}
// 检查文件大小并清空文件(如果超出限制)
fn check_and_clear_log_file() {
let metadata = std::fs::metadata(LOG_FILE_PATH).unwrap();
if metadata.len() > MAX_LOG_SIZE {
// 清空文件
File::create(LOG_FILE_PATH).expect(&format!("Can't re-create file {}", LOG_FILE_PATH));
}
}
#[allow(dead_code)]
pub fn debug(message: &str) {
check_and_clear_log_file();
debug!("{}", message);
}
#[allow(dead_code)]
pub fn info(message: &str) {
check_and_clear_log_file();
info!("{}", message);
}
#[allow(dead_code)]
pub fn warn(message: &str) {
check_and_clear_log_file();
warn!("{}", message);
}
#[allow(dead_code)]
pub fn error(message: &str) {
check_and_clear_log_file();
error!("{}", message);
}

View File

@ -1,6 +1,7 @@
mod encryption;
mod router;
mod sql;
mod logger;
use actix_web::{
rt,
@ -42,10 +43,10 @@ pub struct Args {
#[arg(short, long, default_value_t = 8000, help = "Assets port to listen on")]
asset_port: u16,
#[arg(long, default_value = "./", help = "Path to store database files")]
#[arg(long, default_value = "./data/", help = "Path to store database files")]
path: String,
#[arg(long, default_value = "./asset/", help = "Path to store database files")]
#[arg(long, default_value = "./assets/", help = "Path to store database files")]
asset_path: String,
#[arg(long, default_value_t = false, help = "Enable assets server")]
@ -110,43 +111,47 @@ async fn run_server(in_thread: bool) -> std::io::Result<()> {
let asset_port = args.asset_port;
if args.purge {
println!("Purging accounts...");
let ct = crate::router::userdata::purge_accounts();
println!("Purged {} accounts", ct);
logger::debug("Purging accounts...");
let ct = router::userdata::purge_accounts();
logger::warn(&format!("Purged {} accounts", ct));
}
let webui_server = HttpServer::new(|| App::new()
.wrap_fn(|req, srv| {
println!("WebUI Request: {}", req.path());
srv.call(req)
})
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
.service(css)
.service(js)
.default_service(web::route().to(router::request))
).bind(("0.0.0.0", port))?.run();
let webui_server = HttpServer::new(|| {
App::new()
.wrap_fn(|req, srv| {
let ip = req.peer_addr().map(|addr| addr.ip().to_string()).unwrap_or_else(|| "Unknown IP".to_string());
logger::info(&format!("from {}, WebUI Request: {}", ip, req.path()));
srv.call(req)
})
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
.service(css)
.service(js)
.default_service(web::route().to(router::request))
}).bind(("0.0.0.0", port))?.run();
println!("WebUI Server started: http://0.0.0.0:{}", port);
println!("Database path is set to {}", args.path);
println!("Sif1 transfer requests will attempt to contact NPPS4 at {}", args.npps4);
logger::info(&format!("WebUI Server started: http://0.0.0.0:{}", port));
logger::info(&format!("Database path is set to {}", args.path));
logger::debug(&format!("Sif1 transfer requests will attempt to contact NPPS4 at {}", args.npps4));
let mut asset_server: Option<Server> = None;
if enable_asset_server {
asset_server = Some(HttpServer::new(|| App::new()
.wrap_fn(|req, srv| {
println!("Assets Request: {}", req.path());
srv.call(req)
})
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 1024))
.default_service(web::route().to(router::asset_request))
).bind(("0.0.0.0", asset_port))?.run());
asset_server = Some(HttpServer::new(|| {
App::new()
.wrap_fn(|req, srv| {
let ip = req.peer_addr().map(|addr| addr.ip().to_string()).unwrap_or_else(|| "Unknown IP".to_string());
logger::info(&format!("from {}, Assets Request: {}", ip, req.path()));
srv.call(req)
})
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 1024))
.default_service(web::route().to(router::asset_request))
}).bind(("0.0.0.0", asset_port))?.run());
println!("Assets Server started: http://0.0.0.0:{}", asset_port);
println!("Assets path is set to {}", args.asset_path);
logger::info(&format!("Assets Server started: http://0.0.0.0:{}", asset_port));
logger::info(&format!("Assets path is set to {}", args.asset_path));
}
if args.https {
println!("Note: gree is set to https mode. http requests will fail on jp clients.");
logger::warn("Note: gree is set to https mode. http requests will fail on jp clients.");
}
if in_thread {
@ -161,13 +166,13 @@ async fn run_server(in_thread: bool) -> std::io::Result<()> {
}
while get_running().await {
actix_web::rt::time::sleep(Duration::from_millis(100)).await;
rt::time::sleep(Duration::from_millis(100)).await;
}
handle.stop(false).await;
if handle2.is_some() {
handle2.unwrap().stop(false).await;
}
println!("Stopped");
logger::info("Stopped");
return Ok(());
}
if asset_server.is_some() {
@ -181,10 +186,11 @@ async fn run_server(in_thread: bool) -> std::io::Result<()> {
#[actix_web::main]
async fn stop_server() {
set_running(false).await;
println!("Stopping");
logger::error("Stopping");
}
fn main() -> std::io::Result<()> {
logger::init_logger();
run_server(false)
}

View File

@ -30,13 +30,13 @@ pub mod event_ranking;
use actix_web::{HttpResponse, HttpRequest, http::header::HeaderValue, http::header::HeaderMap, Responder};
use json::{JsonValue, object};
use crate::encryption;
use crate::{encryption, logger};
fn unhandled(req: HttpRequest, body: String) -> Option<JsonValue> {
if body != String::new() {
println!("{}", encryption::decrypt_packet(&body).unwrap_or(body));
logger::error(&format!("{}", encryption::decrypt_packet(&body).unwrap_or(body)));
}
println!("Unhandled request: {}", req.path());
logger::error(&format!("Unhandled request: {}", req.path()));
None
}
@ -195,7 +195,7 @@ pub async fn request(req: HttpRequest, body: String) -> HttpResponse {
"/v1.0/migration/password/register" => gree::migration_password_register(req, body),
"/v1.0/migration" => gree::migration(req, body),
"/api/webui/login" => webui::login(req, body),
"/api/webui/startLoginbonus" => webui::start_loginbonus(req, body),
"/api/webui/submitLoginBonus" => webui::submit_login_bonus(req, body),
"/api/webui/import" => webui::import(req, body),
"/api/webui/set_time" => webui::set_time(req, body),
_ => api_req(req, body).await

View File

@ -2,7 +2,7 @@ use json::{object, array, JsonValue};
use actix_web::{HttpRequest};
use crate::router::{global, items, userdata, databases};
use crate::encryption;
use crate::{encryption, logger};
pub fn add_chat(id: i64, num: i64, chats: &mut JsonValue) -> bool {
for data in chats.members() {
@ -23,7 +23,7 @@ pub fn add_chat(id: i64, num: i64, chats: &mut JsonValue) -> bool {
pub fn add_chat_from_chapter_id(chapter_id: i64, chats: &mut JsonValue) -> bool {
let chapter = &databases::CHAPTERS_MASTER[chapter_id.to_string()];
if chapter.is_empty() {
println!("Attempted to give unknown chapter id {}", chapter_id);
logger::info(&format!("Attempted to give unknown chapter id {}", chapter_id));
return false;
}
add_chat(chapter["masterChatId"].as_i64().unwrap(), chapter["roomId"].as_i64().unwrap(), chats)

View File

@ -1,12 +1,12 @@
use json::{JsonValue, object};
use actix_web::{HttpRequest};
use crate::encryption;
use crate::{encryption, logger};
pub fn error(_req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
println!("client error: {}", body["code"]);
logger::error(&format!("client error: {}", body["code"]));
Some(object!{})
}

View File

@ -164,20 +164,33 @@ pub fn send(mut data: JsonValue, uid: i64, headers: &HeaderMap) -> HttpResponse
HttpResponse::Ok().body(resp)
}
pub fn start_login_bonus(id: i64, bonus: &mut JsonValue) -> bool {
pub fn submit_login_bonus(id: i64, bonus: &mut JsonValue, is_add: bool) -> bool {
if crate::router::login::get_login_bonus_info(id).is_empty() {
return false;
}
for dataa in bonus["bonus_list"].members() {
if dataa["master_login_bonus_id"].as_i64().unwrap() == id {
return false;
if is_add {
for dataa in bonus["bonus_list"].members() {
if dataa["master_login_bonus_id"].as_i64().unwrap() == id {
return false;
}
}
let item = object!{master_login_bonus_id: id,day_counts: [],event_bonus_list: []};
bonus["bonus_list"].push(item).unwrap();
} else {
let mut index_to_remove = None;
for (index, dataa) in bonus["bonus_list"].members().enumerate() {
if dataa["master_login_bonus_id"].as_i64().unwrap() == id {
index_to_remove = Some(index);
break;
}
}
if let Some(index) = index_to_remove {
bonus["bonus_list"].array_remove(index);
}
}
bonus["bonus_list"].push(object!{
master_login_bonus_id: id,
day_counts: [],
event_bonus_list: []
}).unwrap();
true
}

View File

@ -1,7 +1,7 @@
use json::{array, object, JsonValue};
use rand::Rng;
use actix_web::{HttpRequest, http::header::{HeaderMap, HeaderValue}};
use crate::encryption;
use crate::{encryption, logger};
use crate::router::{userdata, global, databases};
@ -113,7 +113,7 @@ pub fn use_item(item: &JsonValue, multiplier: i64, user: &mut JsonValue) {
} else if item["consumeType"] == 4 {
use_itemm(item["value"].as_i64().unwrap(), item["amount"].as_i64().unwrap() * multiplier, user);
} else {
println!("Unknown consume type {}", item["consumeType"]);
logger::error(&format!("Unknown consume type {}", item["consumeType"]));
}
}
@ -142,7 +142,7 @@ pub fn give_gift(data: &JsonValue, user: &mut JsonValue, missions: &mut JsonValu
}
return false;
}
println!("Redeeming reward not implemented for reward type {}", data["reward_type"]);
logger::error(&format!("Redeeming reward not implemented for reward type {}", data["reward_type"]));
false
}
pub fn give_gift_basic(ty_pe: i32, id: i64, amount: i64, user: &mut JsonValue, missions: &mut JsonValue, clear_missions: &mut JsonValue, chats: &mut JsonValue) -> bool {
@ -279,7 +279,7 @@ pub fn get_rarity(id: i64) -> i32 {
pub fn give_character(id: i64, user: &mut JsonValue, missions: &mut JsonValue, clear_missions: &mut JsonValue, chats: &mut JsonValue) -> bool {
let character_rarity = get_rarity(id);
if character_rarity == 0 {
println!("Attempted to give user undefined card!! Card id: {}", id);
logger::error(&format!("Attempted to give user undefined card!! Card id: {}", id));
return false;
}
@ -498,7 +498,7 @@ pub fn use_item_req(req: HttpRequest, body: String) -> Option<JsonValue> {
if item["effectType"].as_i32().unwrap() == 1 {
lp_modification(&mut user, item["effectValue"].as_u64().unwrap() * (amount as u64), false);
} else {
println!("Use item not implemented for effect type {}", item["effectType"]);
logger::error(&format!("Use item not implemented for effect type {}", item["effectType"]));
}
use_item(&object!{
value: body["id"].as_i64().unwrap(),

View File

@ -54,7 +54,7 @@ pub fn bonus(req: HttpRequest, body: String) -> Option<JsonValue> {
let mut bonuses = userdata::get_acc_loginbonus(&key);
if bonuses["bonus_list"].is_empty() {
global::start_login_bonus(1, &mut bonuses);
global::submit_login_bonus(1, &mut bonuses, true);
}
let to_send = do_bonus(&mut user_home, &mut bonuses);
@ -80,7 +80,7 @@ pub fn bonus_event(req: HttpRequest, body: String) -> Option<JsonValue> {
let mut bonuses = userdata::get_acc_eventlogin(&key);
if bonuses["bonus_list"].is_empty() {
global::start_login_bonus(20039, &mut bonuses);
global::submit_login_bonus(20039, &mut bonuses, true);
}
let to_send = do_bonus(&mut user_home, &mut bonuses);

View File

@ -1,182 +1,77 @@
use actix_web::HttpRequest;
use json::{array, object, JsonValue};
use actix_web::{HttpRequest};
use crate::router::{global, userdata, items};
use crate::router::{global, items, userdata};
use crate::encryption;
const UR_ARRAY: [i32; 39] = [40010015, 30010015, 20010018, 10040018, 20050016, 30070015, 40030013,
10070016, 10060018, 20050019, 10020018, 10010014, 10010015, 10010020, 10040016, 10050018,
10080016, 10090015, 20010019, 20030015, 20050014, 20070013, 20080016, 20090013, 20080016,
20090013, 30010017, 30020009, 30040012, 30090009, 30040012, 30090009, 40010011, 40030009,
40040013, 40060010, 40080011, 40090011, 30030010];
const PT_ARRAY: [i32; 112] = [15540001, 15500001, 15500002, 15520001, 15520002, 15520003, 15520004,
15520005, 15520006, 15520007, 15520008, 15520009, 15520010, 15520011, 15520012, 15520013,
15520014, 15520015, 15520016, 15520017, 15520018, 15520019, 15520020, 15510004, 15510005,
15510006, 15510007, 15510008, 15510009, 15510010, 15510011, 15510012, 15510013, 15510014,
15510015, 15510016, 15510017, 15510018, 15510019, 15510020, 15510021, 15510022, 15510023,
15510024, 15530001, 15530002, 15530003, 15530004, 15530005, 15530006, 15530007, 15530008,
15530009, 15530010, 15530011, 15530012, 15530013, 15530014, 15530015, 15530016, 15530017,
15530018, 15530019, 15530020, 15530021, 15530022, 15530023, 15530024, 15530025, 15530026,
15530027, 15530028, 15530029, 15530030, 15530031, 15530032, 15530033, 15530034, 15530035,
15530036, 15530037, 15540002, 15540005, 15540006, 15540007, 15540008, 15540009, 15540010,
15540011, 15540012, 15540013, 15540014, 15540015, 15540016, 15540017, 15540023, 15540024,
15540025, 15540027, 15540028, 15540029, 15540030, 15540031, 15540032, 15540033, 15540034,
15540035, 30010002, 30010003, 30010004, 30010005, 30010001
];
pub fn events(_req: HttpRequest) -> Option<JsonValue> {
Some(object!{
"serial_code_list": []
})
}
fn extract_number_from_code(code: String, goods: &str) -> Option<i64> {
if code.starts_with(goods) {
let number_str = &code[goods.len()..];
match number_str.parse::<i64>() {
Ok(number) => {
if number >= 999999999 {
Some(1)
} else {
Some(number)
}
},
Err(_) => Some(1)
}
} else {
None
}
}
pub fn serial_code(req: HttpRequest, body: String) -> Option<JsonValue> {
let key = global::get_login(req.headers(), &body);
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let mut user = userdata::get_acc_home(&key);
let mut itemz = array![];
if body["input_code"] == "SIF2REVIVALREAL!" {
itemz.push(items::gift_item_basic(1, 10000000, 4, "Another game died... This makes me sad :(", &mut user)).ok()?;
} else if body["input_code"] == "pweasegivegems11" {
itemz.push(items::gift_item_basic(1, 6000, 1, "Only because you asked...", &mut user)).ok()?;
} else if body["input_code"] == "sleepysleepyslep" {
itemz.push(items::gift_item_basic(15540001, 50, 3, "I am tired", &mut user)).ok()?;
} else if body["input_code"] == "ilikeganyu!!!!!!" {
itemz.push(items::gift_item_basic(16005003, 100, 3, "I need more primogems", &mut user)).ok()?;
} else if body["input_code"] == "serial_code" {
itemz.push(items::gift_item_basic(17001003, 100, 3, "nyaa~", &mut user)).ok()?;
} else if body["input_code"] == "ganuy" {
itemz.push(items::gift_item_basic(40010015, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010015, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20010018, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10040018, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20050016, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30070015, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40030013, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10070016, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
} else if body["input_code"] == "kode" {
itemz.push(items::gift_item_basic(10060018, 1, 2, "meow", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20050019, 1, 2, "meow", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10020018, 1, 2, "meow", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10010014, 1, 2, "meow", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10010015, 1, 2, "meow", &mut user)).ok()?;
} else if body["input_code"] == "meow" {
itemz.push(items::gift_item_basic(10010020, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10040016, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10050018, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10080016, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(10090015, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20010019, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20030015, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20050014, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20070013, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20080016, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(20090013, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010017, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30020009, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30040012, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30090009, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40010011, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40030009, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40040013, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40060010, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40080011, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(40090011, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30030010, 1, 2, "I need more primogem!!!!!!", &mut user)).ok()?;
} else if body["input_code"] == "HuTao" {
itemz.push(items::gift_item_basic(15500001, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15500002, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520001, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520002, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520003, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520004, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520005, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520006, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520007, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520008, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520009, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520010, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520011, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520012, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520013, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520014, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520015, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520016, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520017, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520018, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520019, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15520020, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510004, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510005, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510006, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510007, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510008, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510009, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510010, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510011, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510012, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510013, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510014, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510015, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510016, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510017, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510018, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510019, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510020, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510021, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510022, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510023, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15510024, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530001, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530002, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530003, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530004, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530005, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530006, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530007, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530008, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530009, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530010, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530011, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530012, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530013, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530014, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530015, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530016, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530017, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530018, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530019, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530020, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530021, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530022, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530023, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530024, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530025, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530026, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530027, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530028, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530029, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530030, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530031, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530032, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530033, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530034, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530035, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530036, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15530037, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540002, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540005, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540006, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540007, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540008, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540009, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540010, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540011, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540012, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540013, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540014, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540015, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540016, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540017, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540023, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540024, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540025, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540027, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540028, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540029, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540030, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540031, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540032, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540033, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540034, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540035, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010002, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010003, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010004, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010005, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(30010001, 500, 3, "Okay...............", &mut user)).ok()?;
itemz.push(items::gift_item_basic(15540001, 500, 3, "Okay...............", &mut user)).ok()?;
let mut item_list = array![];
let code = body["input_code"].to_string();
if let Some(number) = extract_number_from_code(code.clone(), "coins") {
item_list.push(items::gift_item_basic(1, number, 4, "请亏我全", &mut user)).ok()?;
} else if let Some(number) = extract_number_from_code(code.clone(), "gems") {
item_list.push(items::gift_item_basic(1, number, 1, "你的UR和我的R", &mut user)).ok()?;
} else if let Some(number) = extract_number_from_code(code.clone(), "training") {
item_list.push(items::gift_item_basic(16005003, number, 3, "A~A~O!", &mut user)).ok()?;
item_list.push(items::gift_item_basic(17001003, number, 3, "A~A~O!", &mut user)).ok()?;
} else if let Some(number) = extract_number_from_code(code.clone(), "UR") {
for item in UR_ARRAY {
item_list.push(items::gift_item_basic(item, number, 2, "活动UR+1", &mut user)).ok()?;
}
} else if let Some(number) = extract_number_from_code(code.clone(), "PT") {
for item in PT_ARRAY {
item_list.push(items::gift_item_basic(item, number, 3, "意味わかんない", &mut user)).ok()?;
}
} else {
return Some(object!{
"result_code": 3
@ -189,7 +84,7 @@ pub fn serial_code(req: HttpRequest, body: String) -> Option<JsonValue> {
Some(object!{
"serial_code_event": {"id":1,"name":"Serial Code Reward","unique_limit_count":0,"min_user_rank":0,"max_user_rank":0,"end_date":null},
"reward_list": itemz,
"reward_list": item_list,
"result_code": 0,
"gift_list": user["gift_list"].clone(),
"excluded_gift_list": []

View File

@ -1,12 +1,12 @@
use json::{JsonValue, object};
use actix_web::{HttpRequest, http::header::HeaderValue};
use crate::encryption;
use crate::{encryption, logger};
use crate::router::{userdata, global};
fn get_asset_hash(req: &HttpRequest, body: &JsonValue) -> String {
if body["asset_version"] != global::ASSET_VERSION && body["asset_version"] != global::ASSET_VERSION_JP {
println!("Warning! Asset version is not what was expected. (Did the app update?)");
logger::warn("Warning! Asset version is not what was expected. (Did the app update?)");
}
let blank_header = HeaderValue::from_static("");
@ -29,7 +29,7 @@ pub fn start(req: HttpRequest, body: String) -> Option<JsonValue> {
let body = json::parse(&encryption::decrypt_packet(&body).unwrap()).unwrap();
let mut user = userdata::get_acc(&key);
println!("Signin from uid: {}", user["user"]["id"].clone());
logger::info(&format!("Signin from uid: {}", user["user"]["id"].clone()));
user["user"]["last_login_time"] = global::timestamp().into();

View File

@ -2,7 +2,7 @@ use json::{array, object, JsonValue};
use actix_web::{HttpRequest};
use sha1::{Sha1, Digest};
use crate::encryption;
use crate::{encryption, logger};
use crate::router::{userdata, global, items};
use crate::include_file;
@ -329,7 +329,7 @@ async fn npps4_req(sha_id: String) -> Option<JsonValue> {
host.pop();
}
let url = format!("{}/ewexport?sha1={}", host, sha_id);
println!("Polling NPPS4 at {}", host);
logger::debug(&format!("Polling NPPS4 at {}", host));
let client = reqwest::Client::new();
let response = client.get(url);

View File

@ -8,7 +8,7 @@ use base64::{Engine as _, engine::general_purpose};
use crate::router::global;
use crate::router::items;
use crate::sql::SQLite;
use crate::include_file;
use crate::{get_args, include_file, logger};
lazy_static! {
static ref DATABASE: SQLite = SQLite::new("userdata.db", setup_tables);
@ -543,11 +543,12 @@ pub fn webui_get_user(token: &str) -> Option<JsonValue> {
Some(object!{
userdata: get_acc(&login_token),
loginbonus: get_acc_loginbonus(&login_token),
server_time_set: get_server_data(&login_token)["server_time_set"].clone(),
time: get_server_data(&login_token)["server_time"].clone()
})
}
pub fn webui_start_loginbonus(bonus_id: i64, token: &str) -> JsonValue {
pub fn webui_submit_login_bonus(bonus_id: i64, is_add: bool, token: &str) -> JsonValue {
let login_token = webui_login_token(token);
if login_token.is_none() {
return object!{
@ -557,7 +558,7 @@ pub fn webui_start_loginbonus(bonus_id: i64, token: &str) -> JsonValue {
}
let login_token = login_token.unwrap();
let mut bonuses = get_acc_loginbonus(&login_token);
if !global::start_login_bonus(bonus_id, &mut bonuses) {
if !global::submit_login_bonus(bonus_id, &mut bonuses, is_add) {
return object!{
result: "ERR",
message: "Login bonus ID is either already going or does not exist"
@ -571,7 +572,7 @@ pub fn webui_start_loginbonus(bonus_id: i64, token: &str) -> JsonValue {
}
}
pub fn set_server_time(time: i64, token: &str) -> JsonValue {
pub fn set_server_time(mut time: i64, token: &str) -> JsonValue {
if time as u64 > global::timestamp() {
return object!{
result: "ERR",
@ -588,6 +589,10 @@ pub fn set_server_time(time: i64, token: &str) -> JsonValue {
let login_token = login_token.unwrap();
let mut server_data = get_server_data(&login_token);
server_data["server_time_set"] = global::timestamp().into();
if time == -1 {
let args = get_args();
time = args.max_time as i64;
}
server_data["server_time"] = time.into();
save_server_data(&login_token, server_data);
@ -625,7 +630,7 @@ pub fn purge_accounts() -> usize {
)).unwrap();
for uid in dead_uids.members() {
let user_id = uid.as_i64().unwrap();
println!("Removing dead UID: {}", user_id);
logger::error(&format!("Removing dead UID: {}", user_id));
crate::router::gree::delete_uuid(user_id);
DATABASE.lock_and_exec("DELETE FROM userdata WHERE user_id=?1", params!(user_id));
DATABASE.lock_and_exec("DELETE FROM userhome WHERE user_id=?1", params!(user_id));

View File

@ -97,13 +97,15 @@ pub fn user(req: HttpRequest) -> HttpResponse {
.body(json::stringify(resp))
}
pub fn start_loginbonus(req: HttpRequest, body: String) -> HttpResponse {
pub fn submit_login_bonus(req: HttpRequest, body: String) -> HttpResponse {
let token = get_login_token(&req);
if token.is_none() {
return error("Not logged in");
}
let body = json::parse(&body).unwrap();
let resp = userdata::webui_start_loginbonus(body["bonus_id"].as_i64().unwrap(), &token.unwrap());
let bonus_id = body["bonus_id"].as_i64().unwrap();
let is_add = body["is_add"].as_bool().unwrap();
let resp = userdata::webui_submit_login_bonus(bonus_id, is_add, &token.unwrap());
HttpResponse::Ok()
.insert_header(ContentType::json())
@ -166,7 +168,7 @@ pub async fn asset(req: HttpRequest) -> impl Responder {
if req.path() == "/" {
return HttpResponse::Ok()
.insert_header(ContentType::html())
.body(include_file!("webui/dist/asset_index.html"));
.body(include_file!("src/web/asset_index.html"));
}
let path = req.path();

12
src/web/asset_index.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EW private server</title>
</head>
<body>
<div id="root">Asset Http Server</div>
</body>
</html>

4554
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,9 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"i18next": "^24.2.1",
"react": "^18.2.0",
"react-bootstrap": "^2.10.2",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-i18next": "^15.4.0"
},
"devDependencies": {
"@types/react": "^18.2.66",

View File

@ -1,15 +1,22 @@
import { useState, useParams, useEffect } from 'react'
import {useEffect, useState} from 'react'
import './Help.css'
import Request from '../Request.jsx'
let init = false;
import i18n from 'i18next';
import i18next from "i18next";
import {useTranslation} from "react-i18next";
let init = false
function Help() {
const [downloadUrl, setDownloadUrl] = useState(<div>Your server admin has no pre-patched apks to download. See the question below..</div>);
const [downloadUrl, setDownloadUrl] = useState();
const [defaultDownloadUrl, setDefaultDownloadUrl] = useState();
const [downloadUrliOSGL, setDownloadUrliOSGL] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-gl.ipa");
const [downloadUrliOSJP, setDownloadUrliOSJP] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-jp.ipa");
const [language, setLanguage] = useState()
const { t } = useTranslation();
const [assetUrl, setAssetUrl] = useState("https://sif2.sif.moe");
useEffect(() => {
(async () => {
if (init) return;
@ -21,15 +28,16 @@ function Help() {
if (!resp.data.links) return;
if (resp.data.links.global && resp.data.links.japan) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a> or <a href={resp.data.links.global}>Global</a></div>
<div>{t('has_download_url')}<a href={resp.data.links.japan}>{t('japan')}</a> {t('or')} <a
href={resp.data.links.global}>{t('global')}</a></div>
);
} else if (resp.data.links.global) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.global}>Global</a></div>
<div>{t('has_download_url')}<a href={resp.data.links.global}>{t('global')}</a></div>
);
} else if (resp.data.links.japan) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a></div>
<div>{t('has_download_url')}<a href={resp.data.links.japan}>{t('japan')}</a></div>
);
}
if (resp.data.links.assets) {
@ -46,9 +54,95 @@ function Help() {
})();
});
return (
<div id="home">
useEffect(() => {
setLanguage(i18next.language)
setDefaultDownloadUrl(t('default_download_url'))
setDownloadUrl(t('default_download_url'))
const handleLanguageChanged = (lng) => {
setLanguage(lng)
setDownloadUrl(t('default_download_url'))
};
i18n.on('languageChanged', handleLanguageChanged);
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, []);
const renderNoAndroidDownloadUrl = () => {
if (language === "zh") {
return <>
<h2>我的服务器管理员没有下载链接我该如何修补apk安卓</h2>
<p>你可以用 <a href="https://sif2patch.zhushenwudi.top">@arasfon's sif2 apk 修补工具</a>
服务器URL中填写"{window.location.origin}"资源URL中填写 "{assetUrl}"默认已经帮你填入了以上信息
选择你的游戏版本, 更改头部格式为Lowercase, 最后点击开始修补完成修补后使用 <a
href="https://github.com/patrickfav/uber-apk-signer">uber-apk-signer</a> 来签署apk.
最后便可以安装到你的手机上了</p>
</>
} else {
return <>
<h2>My server admin has no download URLs, how do I patch the apk? (Android)</h2>
<p>You can use <a href="https://sif2patch.zhushenwudi.top">@arasfon's sif2 apk 修补工具</a>.
Plug and the asset url, which is "{assetUrl}", into the "Assets URL" textbox,
and then the server url (Which is likely "{window.location.origin}", though this may not be the case).
Select your game version, set "header format" to "Lowercase", and press patch. Once that done, use
<a href="https://github.com/patrickfav/uber-apk-signer">uber-apk-signer</a>
to sign the apk. Then, get it onto your phone and install!</p>
</>
}
}
const renderZh = () => {
return <div id="home">
<h1>帮助/关于</h1>
<h2>ew是什么这个服务器是用来做什么的</h2>
<p>ew是一个用Rust编写的私人服务器原作者Ethan O'Brien原项目<a
href="https://git.ethanthesleepy.one/ethanaobrien/ew">ew仓库地址</a>
本项目为二次开发版本用于短命的游戏Love Live! School idol festival 2 MIRACLE LIVE!
这是一款以Love Live!为主题的移动节奏游戏</p>
<h2>我只是想在这个服务器上玩我该如何安装这个应用程序安卓</h2>
<p>{downloadUrl}</p>
{downloadUrl === defaultDownloadUrl ? renderNoAndroidDownloadUrl() : null}
<h2>我该如何安装这款应用iOS</h2>
<p>在iOS上运行要比Android简单得多这要归功于Discord上的Triangle你首先要下载一个用于 <a
href={downloadUrliOSGL}>国际服</a> 或者 <a href={downloadUrliOSJP}>日服</a> 的ipa文件,
然后使用 <a href="https://sideloadly.io/">Sideloadly</a> (或任何能够实现侧载的应用程序)来安装这个APP打开手机系统设置,
导航到SIF2应用的设置页面, 填入Server URL"{window.location.origin}" Assets URL"{assetUrl}"
如果您在打开应用程序时遇到任何错误请确保设置中的所有网址都没有以斜杠结尾</p>
<h2>求求我尝试打开SIF2但它显示为不可用iOS</h2>
<p>不要删除当前的SIF2只需重新侧载该应用程序这是苹果的安全功能</p>
<h2>程序的执行效率如何</h2>
<p>运行得相当不错服务器本身占用的存储空间甚至不到20兆字节并且是用Rust编写的我个人认为它写得相当好</p>
<h2>我的电脑/笔记本电脑能运行服务器吗</h2>
<p>非常有可能如果平台是支持 <a
href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">Rust语言</a> ,
那么答案是肯定的在我有时间设置自动操作之前建议手动编译项目 <a
href="https://git.zhushenwudi.top/zhushenwudi/ew">ew仓库地址</a></p>
<h2>服务器现在是宕机了吗我无法连接</h2>
<p>假设你刚刚在你使用的服务器上加载了这个页面那么答案是否定的否则请联系你的服务器管理员</p>
<h2>打歌活动能正常游玩吗</h2>
<p>大多数活动会让游戏崩溃不过这确实是不应该的明星活动部分已实现你可以提升自己的排名并在排名表中与其他玩家竞争但目前尚未实现奖励功能</p>
<h2>我该如何获取活动UR呢</h2>
<p>可以通过游戏内兑换码的方式领取活动URUR</p>
<h2>当我操作窗口关闭时为什么游戏会崩溃</h2>
<p>这很可能意味着服务器上有某些地方出故障了如果你是自行托管服务器请通过Matrix联系我否则联系你的服务器管理员并让他们报告这个问题</p>
</div>
}
const renderEn = () => {
return <div id="home">
<h1>Help/About</h1>
<h2>What is "ew"? What is this server for?</h2>
<p>"ew" is a private server, written in Rust, for the short lived game "Love Live! School idol festival 2 MIRACLE LIVE!", a Love Live! themed mobile rhythm game.</p>
@ -56,11 +150,15 @@ function Help() {
<h2>I'm just trying to play on this server, how do I install the app? (Android)</h2>
<p>{downloadUrl}</p>
<h2>My server admin has no download URLs, how do I patch the apk? (Android)</h2>
<p>You can use <a href="https://arasfon.ru/sif2/patcher/">@arasfon's sif2 apk patcher</a>. Plug and the asset url, which is "{assetUrl}", into the "Assets URL" textbox, and then the server url (Which is likely "{window.location.origin}", though this may not be the case). Select your game version, set "header format" to "Lowercase", and press patch. Once that done, use <a href="https://github.com/patrickfav/uber-apk-signer">uber-apk-signer</a> to sign the apk. Then, get it onto your phone and install!</p>
{downloadUrl === defaultDownloadUrl ? renderNoAndroidDownloadUrl() : null}
<h2>How do I install the app? (iOS)</h2>
<p>Running on iOS is much simpler than Android, thanks to triangle on the discord. You first download an ipa file for <a href={downloadUrliOSGL}>global</a> or <a href={downloadUrliOSJP}>Japan</a>, and use <a href="https://sideloadly.io/">Sideloadly</a> (or your preferred application installer) to install the app. Then open settings, navigate to the app you just installed, and input the server url (Which is likely "{window.location.origin}", though this may not be the case), and the asset url, which is "{assetUrl}". If you have any errors opening the app, make sure none of the urls in settings end with a slash (/).</p>
<p>Running on iOS is much simpler than Android, thanks to triangle on the discord. You first download an ipa file for
<a href={downloadUrliOSGL}>global</a> or <a href={downloadUrliOSJP}>Japan</a>,
and use <a href="https://sideloadly.io/">Sideloadly</a> (or your preferred application installer) to install the app.
Then open settings, navigate to the app you just installed, and input the server url (Which is likely "{window.location.origin}",
though this may not be the case), and the asset url, which is "{assetUrl}".
If you have any errors opening the app, make sure none of the urls in settings end with a slash (/).</p>
<h2>Help! I'm trying to open the app and it shows as "unavailable" (iOS)</h2>
<p>Do not delete it, Just re-sideload the app. This is an Apple security feature.</p>
@ -69,20 +167,27 @@ function Help() {
<p>Works well enough. The server itself takes up not even 20mb of storage, and it's written in rust. I personally think it's pretty well written.</p>
<h2>Could my computer/laptop run a server?</h2>
<p>Very very likely. If the platform is <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">supported by rust</a>, then the answer is yes! It is recommended to manually compile the project until I get the time to setup actions. <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">ew repo</a></p>
<p>Very very likely. If the platform is <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">supported by rust</a>,
then the answer is yes! It is recommended to manually compile the project until I get the time to setup actions.
<a href="https://git.ethanthesleepy.one/ethanaobrien/ew">ew repo</a></p>
<h2>Is the server down right now? I can't connect</h2>
<p>Assuming you have just loaded this page on the server you use, then the answer is no, otherwise please contact your server admin.</p>
<h2>Do events work?</h2>
<p>Most events do not, though most should not crash the game. Star events are partially implemented. You can get your rank up, and compete with other players in a ranking table, but no rewards are currently implemented.</p>
<p>Most events do not, though most should not crash the game. Star events are partially implemented.
You can get your rank up, and compete with other players in a ranking table, but no rewards are currently implemented.</p>
<h2>But then, how do I get event URs?</h2>
<p>There are serial codes for several things, one of which includes all the event URs. I don't remember what does what but it is recommended to look at the serial code file to get the latest codes.</p>
<p>There are serial codes for several things, one of which includes all the event URs. UR</p>
<h2>Why does the game crash when I do x?</h2>
<p>This likely means something on the server is broken. If you're self hosting, please contact me via matrix. Otherwise, contact your server admin and ask them to report the issue.</p>
</div>
}
return (
language === 'zh' ? renderZh() : renderEn()
);
}

View File

@ -1,6 +1,8 @@
import { useState, useParams, useEffect } from 'react'
import { useState, useEffect } from 'react'
import './Home.css'
import Request from '../Request.jsx'
import {useTranslation} from "react-i18next";
let bonusItems = [];
function getMigrationID(uid) {
@ -21,6 +23,7 @@ function getMigrationID(uid) {
function Bonus() {
const [inputValue, setInputValue] = useState('');
const error = useState("");
const { t } = useTranslation();
let itemz = [];
bonusItems.forEach((e) => {
@ -33,8 +36,9 @@ function Bonus() {
event.preventDefault();
let input = parseInt(inputValue.trim());
if (isNaN(input) || submittedItems.includes(input)) return;
let resp = await Request("/api/webui/startLoginbonus", {
bonus_id: input
let resp = await Request("/api/webui/submitLoginBonus", {
bonus_id: input,
is_add: true
});
if (resp.result !== "OK") {
error[1](resp.message);
@ -45,33 +49,43 @@ function Bonus() {
setInputValue('');
};
const handleRemoveItem = (index) => {
const handleRemoveItem = async (index) => {
const updatedItems = [...submittedItems];
let resp = await Request("/api/webui/submitLoginBonus", {
bonus_id: updatedItems[index],
is_add: false
});
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
error[1]("");
updatedItems.splice(index, 1);
setSubmittedItems(updatedItems);
};
// <button onClick={() => handleRemoveItem(index)}>X</button>
return (
<div>
<h2>Current login bonus list</h2>
<h2>{t('current_bonus')}</h2>
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
<ul>
{submittedItems.map((item, index) => (
<li key={index}>
{item}
</li>
<li key={index}>
{item}
<button onClick={() => handleRemoveItem(index)}>X</button>
</li>
))}
</ul>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter login bonus ID"
placeholder={t('enter_bonus_id')}
/>
<button type="submit">Submit</button>
<button type="submit">{t('submit')}</button>
</form>
<p>You can find a list of available login bonus IDs <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/src/router/databases/json/login_bonus.json">here</a>. You should input the <code>id</code> field</p>
<p>{t('find_bonus_in_url')} <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/src/router/databases/json/login_bonus.json">{t('here')}</a>. {t('should_input')} <code>id</code></p>
</div>
);
}
@ -81,6 +95,7 @@ function Home() {
const [inputValue, setInputValue] = useState('');
const [serverTime, setServerTime] = useState('');
const error = useState("");
const { t } = useTranslation();
const logout = () => {
window.location.href = "/webui/logout";
@ -107,12 +122,10 @@ function Home() {
const handleSubmit = async (event) => {
event.preventDefault();
let time = Math.round(new Date(inputValue.trim()).getTime() / 1000);
if (inputValue.trim() === "-1") {
time = 1711741114;
} else if (inputValue.trim() === "0") {
if (inputValue.trim() === "0") {
time = 0;
}
if (time < 0 || isNaN(time)) return;
if (time < -1 || isNaN(time)) return;
let resp = await Request("/api/webui/set_time", {
timestamp: time
});
@ -122,7 +135,7 @@ function Home() {
}
error[1]("");
if (time === 0) {
setServerTime("now");
setServerTime(new Date().toString());
} else {
setServerTime((new Date(time * 1000)).toString());
}
@ -148,20 +161,23 @@ function Home() {
exp: 10,
last_login_time: 5
},
server_time_set: new Date()
time: new Date()
}*/
if (resp.data.time === 0) {
setServerTime("now");
setServerTime(new Date().toString());
} else {
setServerTime((new Date(resp.data.time * 1000)).toString());
let deltaTime = new Date().getTime() - resp.data.server_time_set * 1000
let gameTime = resp.data.time * 1000 + deltaTime
setServerTime((new Date(gameTime)).toString());
}
userdata(
<div>
<p>User id: { user.user.id } </p>
<p>Migration id: { getMigrationID(user.user.id) } </p>
<p>Rank: { user.user.rank } ({ user.user.exp } exp)</p>
<p>Last Login: { (new Date(user.user.last_login_time * 1000)).toString() } </p>
<p>SIF2 ID: { user.user.id } </p>
<p>{t('migration_id')}: { getMigrationID(user.user.id) } </p>
<p>{t('level')}: { user.user.rank } ({ user.user.exp } exp)</p>
<p>{t('last_login_time')}: { (new Date(user.user.last_login_time * 1000)).toString() } </p>
<Bonus />
</div>
);
@ -170,25 +186,25 @@ function Home() {
return (
<div id="home">
<button id="logout" onClick={expor}>Export account</button><br/><br/><br/>
<button id="logout" onClick={logout}>Logout</button>
<h1>Home</h1>
<button id="logout" onClick={expor}>{t('export_account')}</button><br/><br/><br/>
<button id="logout" onClick={logout}>{t('logout')}</button>
<h1>{t('home')}</h1>
{ user ? <div>
{ user }
<h2>Server time</h2>
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
<p>Currently set to { serverTime }. Setting to 0 will set it to now, and -1 will reset it. Time will still progress, based off of when you set this timestamp.</p>
<h2>{t('server_time')}</h2>
<div id="error"> { error[0] ? <p>{t('error')}: { error[0] } </p> : <p></p> } </div>
<p>{t('current_game_time')} { serverTime }. {t("set_time_hint")}</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter Server Time"
placeholder={t('input_server_time')}
/>
<button type="submit">Submit</button>
<button type="submit">{t('submit')}</button>
</form></div>
: <p>Loading...</p> }
: <p>{t('loading')}</p> }
</div>
);
}

View File

@ -1,11 +1,13 @@
import { useState } from 'react'
import './Import.css'
import Request from '../Request.jsx'
import {useTranslation} from "react-i18next";
function Login() {
const error = useState(new URL(window.location).searchParams.get("message") || "");
const status = useState("");
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
const { t } = useTranslation();
let file=[], file1=[], file2=[], file3=[], password;
let has_imported = false;
@ -23,26 +25,26 @@ function Login() {
jp: true
};
if (!data.userdata || !data.userdata.user || !data.userdata.user.id) {
error[1]("Incorrect user data file format");
error[1](t('user_data_incorrect'));
return;
}
if (!data.home || !data.home.home || !data.home.home.information_list) {
error[1]("Incorrect home data file format");
error[1](t('home_data_incorrect'));
return;
}
if (!Array.isArray(data.missions) && data.missions) {
error[1]("Incorrect mission data file format");
error[1](t('mission_data_incorrect'));
return;
}
if (!Array.isArray(data.sif_cards) && data.sif_cards) {
error[1]("Incorrect sif card data file format");
error[1](t('card_data_incorrect'));
return;
}
let resp = await Request(
"/api/webui/import",
data
);
if (resp.result == "OK") {
if (resp.result === "OK") {
status[1](<div><p>Account imported!</p><p>User id: {resp.uid}</p><p>Migration token: {resp.migration_token}</p></div>);
} else {
error[1](resp.message);
@ -54,27 +56,27 @@ function Login() {
return (
<div id="login-form">
<h1>Transfer</h1>
<h1>{t('transfer')}</h1>
<form>
{ <p>{ status[0] } </p> }
<label htmlFor="id">User data file (required):</label>
<label htmlFor="id">userdata.json({t('required')}):</label>
<input type="file" id="id" name="id" onChange={(event) => {file = event.target.files}} accept="application/json"/>
<label htmlFor="file1">User Home data file (required):</label>
<label htmlFor="file1">userhome.json({t('required')}):</label>
<input type="file" id="file1" name="file1" onChange={(event) => {file1 = event.target.files}} accept="application/json"/>
<label htmlFor="file2">User Missions data file (optional):</label>
<label htmlFor="file2">missions.json({t('optional')}):</label>
<input type="file" id="file2" name="file2" onChange={(event) => {file2 = event.target.files}} accept="application/json"/>
<label htmlFor="file3">Sif cards data file (optional):</label>
<label htmlFor="file3">sifcards.json({t('optional')}):</label>
<input type="file" id="file3" name="file3" onChange={(event) => {file3 = event.target.files}} accept="application/json"/>
<label htmlFor="password">Transfer passcode (game will not recognize special characters, only use letters and numbers or you will be locked out):</label>
<label htmlFor="password">{t('transfer_passcode')}{t('transfer_passcode_hint')}</label>
<input type="password" id="password" name="password" onChange={(event) => {password = event.target.value}} />
<input type="submit" value="Submit" onClick={handleSubmit}/>
<input type="submit" value={t('submit')} onClick={handleSubmit}/>
<div id="sub_div">
{ error[0] ? <p>Error: { error[0] }. Please reload the page and try again.</p> : <p></p> }
{ error[0] ? <p>{t('error')}: { error[0] }. {t('refresh_and_retry')}</p> : <p></p> }
</div>
</form>
</div>

43
webui/src/locales/en.json Normal file
View File

@ -0,0 +1,43 @@
{
"login": "Login",
"need_full_input": "Please fill in the information completely",
"uid_not_correct": "SIF2 ID should be a number, get it in game personal profile",
"transfer_passcode": "Transfer passcode:",
"submit": "Submit",
"import_user": "Import User",
"need_help": "Need help?",
"ew_version": "EW version",
"view_source": "View source",
"view_license": "View license",
"current_bonus": "Current login bonus list",
"enter_bonus_id": "Enter login bonus ID",
"find_bonus_in_url": "You can find a list of available login bonus IDs",
"here": "here",
"should_input": "You should input the",
"migration_id": "Migration id",
"level": "Level",
"last_login_time": "Last Login",
"export_account": "Export account",
"logout": "Logout",
"home": "Home",
"server_time": "Server time",
"error": "Error",
"current_game_time": "Currently set to",
"set_time_hint": "Setting to 0 will set it to now, and -1 will reset to open server time. Time will still progress, based off of when you set this timestamp.",
"input_server_time": "Enter Server Time",
"loading": "Loading...",
"transfer": "Transfer",
"required": "required",
"optional": "optional",
"transfer_passcode_hint": "(game will not recognize special characters, only use letters and numbers or you will be locked out)",
"refresh_and_retry": "Please reload the page and try again.",
"user_data_incorrect": "Incorrect user data file format",
"home_data_incorrect": "Incorrect home data file format",
"mission_data_incorrect": "Incorrect mission data file format",
"card_data_incorrect": "Incorrect sif card data file format",
"default_download_url": "Your server admin has no pre-patched apks to download. See the question below.",
"has_download_url": "Your server admin has a link to download! Download ",
"japan": "Japan",
"global": "Global",
"or": "Or"
}

43
webui/src/locales/zh.json Normal file
View File

@ -0,0 +1,43 @@
{
"login": "登录",
"need_full_input": "请完整填入信息",
"uid_not_correct": "SIF2 ID必须是数字在游戏中个人详情页中获取",
"transfer_passcode": "数据迁移密钥:",
"submit": "提交",
"import_user": "导入用户数据",
"need_help": "需要帮助?",
"ew_version": "EW 版本号",
"view_source": "查看源代码",
"view_license": "查看许可证",
"current_bonus": "当前登录奖励列表",
"enter_bonus_id": "输入登录奖励ID",
"find_bonus_in_url": "你可以找到可用登录奖励ID的列表",
"here": "这里",
"should_input": "你应该输入指定",
"migration_id": "引继码",
"level": "等级",
"last_login_time": "上次登录",
"export_account": "导出用户数据",
"logout": "登出",
"home": "用户主页",
"server_time": "服务器时间",
"error": "错误",
"current_game_time": "当前游戏时间为",
"set_time_hint": "将其设置为0将会设置为当前时间而-1则会重置到开服时间。时间的推进仍将基于您设置此时间戳的时间。",
"input_server_time": "输入服务器时间",
"loading": "请稍候...",
"transfer": "导入数据",
"required": "必要",
"optional": "可选",
"transfer_passcode_hint": "(游戏将无法识别特殊字符,只能使用字母和数字,否则您将被锁定)",
"refresh_and_retry": "请重新加载页面并重试。",
"user_data_incorrect": "用户数据 文件格式不正确",
"home_data_incorrect": "主页数据 文件格式不正确",
"mission_data_incorrect": "任务数据 文件格式不正确",
"card_data_incorrect": "卡牌数据 文件格式不正确",
"default_download_url": "您的服务器管理员没有可供下载的预打补丁的应用程序包。请参阅下面的问题。",
"has_download_url": "您的服务器管理员有下载链接!下载 ",
"japan": "日服",
"global": "国际服",
"or": "或者"
}

View File

@ -1,11 +1,13 @@
import { useState } from 'react'
import {useState} from 'react'
import './Login.css'
import Request from '../Request.jsx'
import {useTranslation} from "react-i18next";
function Login() {
const error = useState(new URL(window.location).searchParams.get("message") || "");
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
const password = useState("");
const { t } = useTranslation();
function showError(message) {
error[1](message);
@ -14,11 +16,11 @@ function Login() {
const handleSubmit = async (event) => {
event.preventDefault();
if (!uid[0] || !password[0]) {
showError("Missing userid/password");
showError(t('need_full_input'));
return;
}
if (isNaN(uid[0])) {
showError("UserID should be a number. (The \"Friend ID\" in your profile)");
showError(t('uid_not_correct'));
return;
}
if (window.localStorage) window.localStorage.setItem("ew_uid", uid[0]);
@ -29,7 +31,7 @@ function Login() {
password: password[0]
}
);
if (resp.result == "OK") {
if (resp.result === "OK") {
window.location.href = "/home/";
} else {
showError(resp.message);
@ -48,19 +50,26 @@ function Login() {
return (
<div id="login-form">
<h1>Login</h1>
<h1>{t('login')}</h1>
<form>
<label htmlFor="id">SIF2 ID:</label>
<input type="text" id="id" name="id" onChange={(event) => {uid[1](event.target.value)}} value={uid[0]} />
<label htmlFor="password">Transfer passcode:</label>
<input type="password" id="password" name="password" onChange={(event) => {password[1](event.target.value)}} />
<input type="submit" value="Submit" onClick={handleSubmit}/>
<input type="text" id="id" name="id" onChange={(event) => {
uid[1](event.target.value)
}} value={uid[0]}/>
<label htmlFor="password">{t('transfer_passcode')}</label>
<input type="password" id="password" name="password" onChange={(event) => {
password[1](event.target.value)
}}/>
<input type="submit" value={t('submit')} onClick={handleSubmit}/>
<div id="sub_div">
<button onClick={import_user}>Import User</button><br/><br/>
<button onClick={help}>Need help?</button><br/><br/>
{ error[0] ? <p>Error: { error[0] } </p> : <p></p> }
<button onClick={import_user}>{t('import_user')}</button>
<br/><br/>
<button onClick={help}>{t('need_help')}</button>
<br/><br/>
{error[0] ? <p>Error: {error[0]} </p> : <p></p>}
</div>
<p>EW Version 1.0.0 - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">View source</a> - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/LICENSE">View license</a></p>
<p>{t('ew_version')} 1.0.1 - <a href="https://git.zhushenwudi.top/zhushenwudi/ew">{t('view_source')}</a> - <a
href="https://git.zhushenwudi.top/zhushenwudi/ew/src/branch/main/LICENSE">{t('view_license')}</a></p>
</form>
</div>
);

View File

@ -4,6 +4,11 @@ import Login from './login/Login.jsx'
import Home from './home/Home.jsx'
import Import from './import/Import.jsx'
import Help from './help/Help.jsx'
import LanguageSwitcher from "./utils/LanguageSwitcher.jsx";
import {I18nextProvider} from 'react-i18next';
import i18next from 'i18next';
import enTranslation from './locales/en.json';
import zhTranslation from './locales/zh.json';
let Elem;
switch (window.location.pathname) {
@ -17,16 +22,31 @@ switch (window.location.pathname) {
Elem = Import;
break;
case "/help/":
Elem = Help;
break;
Elem = Help;
break;
default:
window.location.pathname = "/";
}
if (Elem) {
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Elem />
</React.StrictMode>,
)
}
const savedLanguage = localStorage.getItem('language') || 'zh';
i18next.init({
interpolation: {escapeValue: false},
lng: savedLanguage,
resources: {
en: {translation: enTranslation},
zh: {translation: zhTranslation},
},
}).then(() => {
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<I18nextProvider i18n={i18next}>
<div style={{position: 'relative'}}>
<LanguageSwitcher/>
<Elem/>
</div>
</I18nextProvider>
</React.StrictMode>
);
});
}

View File

@ -0,0 +1,56 @@
import {useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
function LanguageSwitcher() {
const {i18n} = useTranslation();
const [margin, setMargin] = useState(0);
const [language, setLanguage] = useState(() => {
return localStorage.getItem('language') || 'zh'; // localStorage
});
useEffect(() => {
i18n.changeLanguage(language);
localStorage.setItem('language', language); // localStorage
}, [language, i18n]);
useEffect(() => {
changeMargin()
}, []);
useEffect(() => {
const handleResize = () => {
changeMargin()
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const changeMargin = () => {
switch (window.location.pathname) {
case "/home/":
case "/help/":
setMargin((window.innerWidth - 228) * 0.05);
break;
default:
setMargin((window.innerWidth - 418) / 2);
break;
}
}
const changeLanguage = () => {
const newLanguage = language === 'zh' ? 'en' : 'zh';
setLanguage(newLanguage);
};
return (
<button style={{position: "absolute", top: 0, right: margin, zIndex: 100}} onClick={changeLanguage}>
{language === "zh" ? "English" : "中文"}
</button>
);
}
export default LanguageSwitcher;