diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b08973..9542554a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] + +### Added + +- New feature for automatically accepting invite requests while on orange/green status, optionally based on a white- or blacklist. +- Korean language support, thanks to [@soumt-r](https://github.com/soumt-r). + +### Changed + +- Made sleeping position animation automations automatically trigger when the automation is enabled. +- Prevent Oyasumi from being opened twice and instead focus the window for the instance already running. + ## [1.2.2] ### Fixed + - Fixed issue where the main window would load before the app was ready, due to a bug in a new version of the `tao` crate. ## [1.2.1] diff --git a/README.md b/README.md index ec29bd10..ca597f70 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@

- This is the main repository for Oyasumi.
It is an open source tool to assist with sleeping in virtual reality (VRChat). + Oyasumi is an open source tool to assist with sleeping in virtual reality (VRChat). +
+ Oyasumiは、バーチャルリアリティの中で睡眠をアシストするオープンソースソフトウェアです。 +
+ booth.pmのストアページには、日本語の説明文が掲載されています。

@@ -28,9 +32,39 @@ Grab the latest installer over on the [Releases](https://github.com/Raphiiko/Oyasumi/releases) page. -|Sleeping Animations |GPU Power Limiting |Sleep Detection |Battery Automations |Device Overview |Status Automations | -|-------------------------------------------------------------------------------------------------------------------------------- |---------------------------------------------------------------------------------------------------------------------------- |----------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------| -| | ||||| +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Device Overview

Sleeping Animations

GPU Power Limiting

Sleep Detection

Battery Automations

Status Automations

General Settings

Auto Accept Invite Requests

+

## Features @@ -57,6 +91,9 @@ Grab the latest installer over on the [Releases](https://github.com/Raphiiko/Oya - Automatically change your status based on the number of players in your world:
_Switch to blue when you are sleeping alone so your friends can join you, and switch to orange when there's enough people around!_ - :wrench: Turning off all trackers and/or controllers with a single click. +- :email: Automatically accept invite requests + - Automatically let friends in while you are asleep! + - Configure whose invite requests are accepted using a black- or whitelist. - :zzz: Manage automations with a sleep mode in various ways: - Detect falling asleep: - When a controller or tracker battery percentage falls below a threshold @@ -70,6 +107,7 @@ Grab the latest installer over on the [Releases](https://github.com/Raphiiko/Oya - English - Dutch (Nederlands) - Japanese (日本語) + - Korean (한국어) (Community contribution by [Soumt](https://github.com/soumt-r)) If you would like to help out with adding more languages and/or missing translations, please check out [the wiki page on adding translations](https://github.com/Raphiiko/Oyasumi/wiki/Adding-Translations) for instructions on how to get started! diff --git a/docs/img/screenshot_auto_accept_invite_requests.png b/docs/img/screenshot_auto_accept_invite_requests.png new file mode 100644 index 00000000..a8f0b0be Binary files /dev/null and b/docs/img/screenshot_auto_accept_invite_requests.png differ diff --git a/docs/img/screenshot_friend_select.png b/docs/img/screenshot_friend_select.png new file mode 100644 index 00000000..2f100d2e Binary files /dev/null and b/docs/img/screenshot_friend_select.png differ diff --git a/package.json b/package.json index a962b49a..f10b54be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oyasumi", - "version": "1.2.2", + "version": "1.3.0", "author": "Raphiiko", "license": "MIT", "type": "module", @@ -26,6 +26,7 @@ "@angular/router": "^14.2.9", "@fontsource/fira-code": "^4.5.12", "@fontsource/noto-sans-jp": "^4.5.12", + "@fontsource/noto-sans-kr": "^4.5.12", "@fontsource/poppins": "^4.5.10", "@fortawesome/fontawesome-free": "^6.2.0", "@ngx-translate/core": "^14.0.0", @@ -33,6 +34,7 @@ "@tauri-apps/api": "^1.2.0", "cookie": "^0.5.0", "flag-icons": "^6.6.6", + "fuse.js": "^6.6.2", "lodash": "^4.17.21", "marked": "^4.2.2", "material-icons": "^1.12.1", diff --git a/src-elevated-sidecar/Cargo.toml b/src-elevated-sidecar/Cargo.toml index a7ab2dbb..d7716b87 100644 --- a/src-elevated-sidecar/Cargo.toml +++ b/src-elevated-sidecar/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oyasumi-elevated-sidecar" -version = "1.2.2" +version = "1.3.0" authors = ["Raphiiko"] license = "MIT" edition = "2021" diff --git a/src-shared/Cargo.toml b/src-shared/Cargo.toml index 0192564f..ecd135cf 100644 --- a/src-shared/Cargo.toml +++ b/src-shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oyasumi-shared" -version = "1.2.2" +version = "1.3.0" authors = ["Raphiiko"] edition = "2021" license = "MIT" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ecfb7d39..2c17fc27 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -53,6 +53,100 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atk" version = "0.15.1" @@ -351,6 +445,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -622,6 +725,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -645,6 +759,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -655,6 +778,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -714,6 +848,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "1.8.0" @@ -852,6 +1013,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.25" @@ -1228,6 +1404,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "html5ever" version = "0.25.2" @@ -1639,6 +1821,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" @@ -1660,6 +1848,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1754,6 +1952,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1979,6 +2191,16 @@ dependencies = [ "cmake", ] +[[package]] +name = "ordered-stream" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.1.1" @@ -1997,7 +2219,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oyasumi" -version = "1.2.2" +version = "1.3.0" dependencies = [ "chrono", "cronjob", @@ -2005,6 +2227,9 @@ dependencies = [ "hyper", "lazy_static", "log", + "md5", + "mime", + "mime_guess", "openvr", "openvr_sys", "oyasumi-shared", @@ -2017,16 +2242,19 @@ dependencies = [ "tauri-build", "tauri-plugin-fs-extra", "tauri-plugin-log", + "tauri-plugin-single-instance", "tauri-plugin-store", "time 0.3.17", "tokio", + "url", + "urlencoding", "winapi", "windows-sys 0.36.1", ] [[package]] name = "oyasumi-shared" -version = "1.2.2" +version = "1.3.0" dependencies = [ "openvr", "serde", @@ -2057,6 +2285,12 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2262,6 +2496,20 @@ dependencies = [ "miniz_oxide 0.6.2", ] +[[package]] +name = "polling" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2824,6 +3072,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.6" @@ -2937,6 +3196,12 @@ dependencies = [ "loom", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.4" @@ -3214,6 +3479,16 @@ dependencies = [ "time 0.3.17", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/tauri-plugin-single-instance?branch=dev#941e10ec8d00fe58d7a25d22d71a5eba80bb9bf5" +dependencies = [ + "tauri", + "windows-sys 0.42.0", + "zbus", +] + [[package]] name = "tauri-plugin-store" version = "0.0.0" @@ -3575,6 +3850,25 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -3620,6 +3914,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "utf-8" version = "0.7.6" @@ -3687,6 +3987,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -3887,6 +4193,15 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4272,6 +4587,69 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "zbus" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaaf011914a4f69a9ec6361225d8440be69673bda9c888275da99c6e8aad03f" +dependencies = [ + "async-broadcast", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64de32c103d3a845f37adf0f46397d6346c8d48028e218db44dcde981733880" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zip" version = "0.6.3" @@ -4282,3 +4660,29 @@ dependencies = [ "crc32fast", "crossbeam-utils", ] + +[[package]] +name = "zvariant" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 387313af..6da31a9e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oyasumi" -version = "1.2.2" +version = "1.3.0" description = "" authors = ["Raphiiko"] license = "MIT" @@ -29,6 +29,11 @@ sysinfo = "0.26.2" chrono = "0.4.22" log = "^0.4" time = "0.3.16" +url = "2.3.1" +urlencoding = "2.1.2" +mime = "0.3.16" +md5 = "0.7.0" +mime_guess = "2.0.4" [dependencies.windows-sys] version = "0.36.1" @@ -46,6 +51,10 @@ features = ["full"] version = "0.14.20" features = ["full"] +[dependencies.tauri-plugin-single-instance] +git = "https://github.com/tauri-apps/tauri-plugin-single-instance" +branch = "dev" + [dependencies.tauri-plugin-store] git = "https://github.com/tauri-apps/tauri-plugin-store" branch = "dev" diff --git a/src-tauri/src/background/http_server.rs b/src-tauri/src/background/http_server.rs index 85660121..869cb82a 100644 --- a/src-tauri/src/background/http_server.rs +++ b/src-tauri/src/background/http_server.rs @@ -4,9 +4,9 @@ use hyper::{ service::{make_service_fn, service_fn}, Body, Method, Request, Response, Server, }; -use log::{info, error}; +use log::{error, info}; -use crate::{elevated_sidecar, MAIN_HTTP_SERVER_PORT}; +use crate::{elevated_sidecar, MAIN_HTTP_SERVER_PORT, IMAGE_CACHE}; pub fn spawn_http_server_thread() { thread::spawn(move || { @@ -36,6 +36,14 @@ async fn start_server() { async fn request_handler(req: Request) -> Result, Infallible> { match (req.method(), req.uri().path()) { + (&Method::GET, "/image_cache/get") => { + let image_cache; + { + let image_cache_guard = IMAGE_CACHE.lock().unwrap(); + image_cache = image_cache_guard.as_ref().unwrap().clone(); + } + image_cache.clone().handle_request(req).await + } (&Method::POST, "/elevated_sidecar/init") => { elevated_sidecar::handle_elevated_sidecar_init(req).await } diff --git a/src-tauri/src/commands/http.rs b/src-tauri/src/commands/http.rs new file mode 100644 index 00000000..053648b5 --- /dev/null +++ b/src-tauri/src/commands/http.rs @@ -0,0 +1,10 @@ +use crate::MAIN_HTTP_SERVER_PORT; + +#[tauri::command] +pub fn get_http_server_port() -> Option { + let port_guard = MAIN_HTTP_SERVER_PORT.lock().unwrap(); + match port_guard.as_ref() { + Some(port) => Some(*port), + None => None, + } +} diff --git a/src-tauri/src/image_cache.rs b/src-tauri/src/image_cache.rs new file mode 100644 index 00000000..c46a6080 --- /dev/null +++ b/src-tauri/src/image_cache.rs @@ -0,0 +1,346 @@ +use hyper::{ + header::{CONTENT_TYPE, USER_AGENT}, + Body, Request, Response, +}; +use log::{error, info}; +use mime::Mime; +use mime_guess; +use serde_json::json; +use std::{collections::HashMap, convert::Infallible, ffi::OsString, path::Path, str::FromStr}; +use urlencoding::decode; + +#[derive(Debug, Clone)] +pub struct ImageCache { + cache_path_str: OsString, +} + +impl ImageCache { + pub fn new(cache_path_str: OsString) -> ImageCache { + ImageCache { cache_path_str } + } + + fn get_image(&self, url: String) -> Option<(Vec, Mime)> { + // Determine paths + let url_hash = format!("{:x}", md5::compute(url)); + let storage_path = Path::new(&self.cache_path_str).join(url_hash); + let manifest_path = storage_path.join("manifest.json"); + // If storage directory or the manifest don't exist, return None + if !storage_path.exists() || !manifest_path.exists() { + return None; + } + // Read json from manifest + let manifest = match std::fs::read_to_string(&manifest_path) { + Ok(manifest) => manifest, + Err(_) => { + error!( + "[Core] Could not read JSON from manifest file. {}", + manifest_path.display() + ); + return None; + } + }; + let manifest: serde_json::Value = serde_json::from_str(&manifest).unwrap(); + // Get filename from manifest + let file_name = match manifest["filename"].as_str() { + Some(file_name) => file_name, + None => { + error!( + "[Core] Could not get filename from manifest file. {}", + manifest_path.display() + ); + return None; + } + }; + // Get image path + let image_path = storage_path.join(file_name); + // If image doesn't exist, return None + if !image_path.exists() { + return None; + } + // Check if image is expired + let ttl = match manifest["ttl"].as_u64() { + Some(ttl) => ttl, + None => { + error!( + "[Core] Could not get TTL from manifest file. {}", + manifest_path.display() + ); + return None; + } + }; + let created = match manifest["created"].as_u64() { + Some(created) => created, + None => { + error!( + "[Core] Could not get created timestamp from manifest file. {}", + manifest_path.display() + ); + return None; + } + }; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + if now - created > ttl { + return None; + } + // Get mime type from manifest + let mime = match manifest["mime"].as_str() { + Some(mime) => match Mime::from_str(mime) { + Ok(mime) => mime, + Err(_) => { + error!( + "[Core] Could not parse MIME type from manifest file. {}", + manifest_path.display() + ); + return None; + } + }, + None => { + error!( + "[Core] Could not get MIME type from manifest file. {}", + manifest_path.display() + ); + return None; + } + }; + // Read image data + let image_data = match std::fs::read(&image_path) { + Ok(image_data) => image_data, + Err(_) => { + error!( + "[Core] Could not read image data from file. {}", + image_path.display() + ); + return None; + } + }; + // Return image data and mime type + Some((image_data, mime)) + } + + fn store_image(&self, url: &str, ttl: u64, mime: Mime, image_data: Vec) { + // Determine paths + let url_hash = format!("{:x}", md5::compute(url)); + let storage_path = Path::new(&self.cache_path_str).join(&url_hash); + let manifest_path = storage_path.join("manifest.json"); + let file_ext = self.get_ext_for_mime(mime.clone()); + let file_name = format!("image.{}", file_ext); + let image_path = storage_path.join(&file_name); + // Delete current storage directory if it exists + if storage_path.exists() { + std::fs::remove_dir_all(&storage_path).unwrap(); + } + // Create storage directory + std::fs::create_dir_all(&storage_path).unwrap(); + // Store image + std::fs::write(&image_path, image_data).unwrap(); + // Store manifest + let manifest = json!({ + "url": url, + "hash": url_hash.clone(), + "ttl": ttl, + "mime": mime.to_string(), + "created": chrono::Utc::now().timestamp(), + "filename": file_name.clone(), + }); + std::fs::write(&manifest_path, manifest.to_string()).unwrap(); + } + + pub fn clean(&self, only_expired: bool) { + // Create directory at cache_path if it doesn't exist + let cache_path = Path::new(&self.cache_path_str); + if !cache_path.exists() { + std::fs::create_dir_all(&cache_path).unwrap(); + return; + } + let mut deleted = 0; + // Iterate over all directories in cache_path + for entry in std::fs::read_dir(&cache_path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + // Skip if path is not a directory + if !path.is_dir() { + continue; + } + // Read manifest + let manifest_path = path.join("manifest.json"); + let manifest = match std::fs::read_to_string(&manifest_path) { + Ok(manifest) => manifest, + Err(_) => { + error!( + "[Core] Could not read JSON from manifest file. {}", + manifest_path.display() + ); + // Delete path if manifest could not be read + std::fs::remove_dir_all(&path).unwrap(); + continue; + } + }; + let manifest: serde_json::Value = serde_json::from_str(&manifest).unwrap(); + // Check if image is expired + let ttl = match manifest["ttl"].as_u64() { + Some(ttl) => ttl, + None => { + error!( + "[Core] Could not get TTL from manifest file. {}", + manifest_path.display() + ); + continue; + } + }; + let created = match manifest["created"].as_u64() { + Some(created) => created, + None => { + error!( + "[Core] Could not get created timestamp from manifest file. {}", + manifest_path.display() + ); + continue; + } + }; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + if only_expired && now - created < ttl { + continue; + } + // Delete storage directory + std::fs::remove_dir_all(&path).unwrap(); + deleted += 1; + } + if deleted > 0 { + info!("[Core] Deleted {} image(s) from the cache.", deleted); + } + } + + fn get_ext_for_mime(&self, mime: Mime) -> String { + match mime_guess::get_mime_extensions(&mime) { + Some(exts) => exts[0].to_string(), + None => "bin".to_string(), + } + } + + pub async fn handle_request(&self, req: Request) -> Result, Infallible> { + // Parse query parameters + let params: HashMap = req + .uri() + .query() + .map(|v| { + url::form_urlencoded::parse(v.as_bytes()) + .into_owned() + .collect() + }) + .unwrap_or_else(HashMap::new); + + // Get URL parameter + let url = match params.get("url") { + Some(url) => decode(url).expect("UTF-8"), + None => { + return Ok(Response::builder() + .status(400) + .body("Missing 'url' query parameter".into()) + .unwrap()) + } + }; + // Get ttl parameter + let ttl = match params.get("ttl") { + Some(ttl) => match ttl.parse::() { + Ok(ttl) => ttl, + Err(_) => { + return Ok(Response::builder() + .status(400) + .body("Invalid 'ttl' query parameter".into()) + .unwrap()) + } + }, + None => { + return Ok(Response::builder() + .status(400) + .body("Missing 'ttl' query parameter".into()) + .unwrap()) + } + }; + // Return cached data if present + if let Some((image_data, image_mime)) = self.get_image(String::from(url.as_ref())) { + return Ok(Response::builder() + .status(200) + .header(CONTENT_TYPE, image_mime.to_string()) + .body(image_data.into()) + .unwrap()); + } + // Get image from URL + let client = reqwest::Client::new(); + let (image_data, image_mime) = match client + .get(url.as_ref()) + .header( + USER_AGENT, + format!( + "Oyasumi/{} (https://github.com/Raphiiko/Oyasumi)", + env!("CARGO_PKG_VERSION"), + ), + ) + .send() + .await + { + Ok(response) => { + let headers = response.headers().clone(); + let bytes = response.bytes(); + match bytes.await { + Ok(bytes) => { + let content_type = match headers.get(CONTENT_TYPE) { + None => { + return Ok(Response::builder() + .status(500) + .body("Failed to get image content type (1)".into()) + .unwrap()) + } + Some(content_type) => { + let content_type_str = match content_type.to_str() { + Ok(content_type_str) => content_type_str, + Err(_) => { + return Ok(Response::builder() + .status(500) + .body("Failed to get image content type (2)".into()) + .unwrap()) + } + }; + match Mime::from_str(content_type_str) { + Ok(content_type) => content_type, + Err(_) => { + return Ok(Response::builder() + .status(500) + .body("Failed to get image content type (3)".into()) + .unwrap()) + } + } + } + }; + (bytes.to_vec(), content_type) + } + Err(_) => { + return Ok(Response::builder() + .status(500) + .body("Failed to get image data".into()) + .unwrap()) + } + } + } + Err(_) => { + return Ok(Response::builder() + .status(500) + .body("Failed to get image".into()) + .unwrap()) + } + }; + // Cache image + self.store_image(url.as_ref(), ttl, image_mime, image_data.clone()); + // Return image + Ok(Response::builder() + .status(200) + .body(image_data.into()) + .unwrap()) + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 15b5af98..a1bb72c3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,10 +6,11 @@ #[macro_use(lazy_static)] extern crate lazy_static; +use crate::image_cache::ImageCache; use cronjob::CronJob; use log::{error, info, LevelFilter}; use std::{net::UdpSocket, sync::Mutex}; -use tauri::Manager; +use tauri::{api::dialog::blocking::MessageDialogBuilder, Manager}; use tauri_plugin_fs_extra::FsExtra; use tauri_plugin_log::{LogTarget, LoggerBuilder, RotationStrategy}; use tauri_plugin_store::PluginBuilder; @@ -22,6 +23,7 @@ mod commands { pub mod os; pub mod osc; pub mod splash; + pub mod http; } mod background { pub mod http_server; @@ -30,6 +32,7 @@ mod background { pub mod osc; } mod elevated_sidecar; +mod image_cache; lazy_static! { static ref OVR_CONTEXT: Mutex> = Default::default(); @@ -40,6 +43,7 @@ lazy_static! { static ref MAIN_HTTP_SERVER_PORT: Mutex> = Default::default(); static ref SIDECAR_HTTP_SERVER_PORT: Mutex> = Default::default(); static ref SIDECAR_PID: Mutex> = Default::default(); + static ref IMAGE_CACHE: Mutex> = Default::default(); } fn main() { @@ -65,6 +69,15 @@ fn main() { .rotation_strategy(RotationStrategy::KeepAll) .build(), ) + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + // Focus main window when user attempts to launch a second instance. + let window = app.get_window("main").unwrap(); + if let Some(is_visible) = window.is_visible().ok() { + if is_visible { + window.set_focus().unwrap(); + } + } + })) .setup(|app| { // Set up window reference let window = app.get_window("main").unwrap(); @@ -73,7 +86,15 @@ fn main() { window.open_devtools(); } *TAURI_WINDOW.lock().unwrap() = Some(window); - std::thread::spawn(|| -> () { + // Get dependencies + let cache_dir = app.path_resolver().app_cache_dir().unwrap().clone(); + let app_handle = app.handle(); + std::thread::spawn(move || -> () { + // Initialize Image Cache + let image_cache_dir = cache_dir.join("image_cache"); + let image_cache = ImageCache::new(image_cache_dir.into_os_string().clone()); + image_cache.clean(true); + *IMAGE_CACHE.lock().unwrap() = Some(image_cache); // Initialize OpenVR info!("[Core] Initializing OpenVR"); let ovr_context = match unsafe { openvr::init(openvr::ApplicationType::Overlay) } { @@ -84,22 +105,24 @@ fn main() { let window_guard = TAURI_WINDOW.lock().unwrap(); let window = window_guard.as_ref().unwrap(); let _ = window.emit_all("OVR_INIT_FAILED", ()); + MessageDialogBuilder::new("Oyasumi", "Could not connect to SteamVR. Please make sure SteamVR is installed before launching Oyasumi.").show(); + app_handle.exit(1); None } }; - // Spawn event handling thread *OVR_CONTEXT.lock().unwrap() = ovr_context; let context_guard = OVR_CONTEXT.lock().unwrap(); let ovr_context = context_guard.as_ref(); if let Some(_) = ovr_context { + // Spawn event handling thread background::openvr::spawn_openvr_background_thread(); + // Inform frontend of completion + info!("[Core] OpenVR initialization complete"); + *OVR_STATUS.lock().unwrap() = String::from("INIT_COMPLETE"); + let window_guard = TAURI_WINDOW.lock().unwrap(); + let window = window_guard.as_ref().unwrap(); + let _ = window.emit_all("OVR_INIT_COMPLETE", ()); } - // Inform frontend of completion - info!("[Core] OpenVR initialization complete"); - *OVR_STATUS.lock().unwrap() = String::from("INIT_COMPLETE"); - let window_guard = TAURI_WINDOW.lock().unwrap(); - let window = window_guard.as_ref().unwrap(); - let _ = window.emit_all("OVR_INIT_COMPLETE", ()); // Spawn HTTP server thread background::http_server::spawn_http_server_thread(); }); @@ -125,6 +148,7 @@ fn main() { commands::admin::elevation_sidecar_running, commands::admin::start_elevation_sidecar, commands::log_parser::init_vrc_log_watcher, + commands::http::get_http_server_port, ]) .run(tauri::generate_context!()) .expect("An error occurred while running the application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 83e948bf..930ce3e3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "Oyasumi", - "version": "1.2.2" + "version": "1.3.0" }, "tauri": { "allowlist": { @@ -35,7 +35,9 @@ "http": { "request": true, "scope": [ - "https://api.vrchat.cloud/api/1/*" + "https://api.vrchat.cloud/api/1/*", + "https://assets.vrchat.com/*", + "https://files.vrchat.cloud/*" ] } }, @@ -76,7 +78,8 @@ "language": [ "en-US", "nl-NL", - "ja-JP" + "ja-JP", + "ko-KR" ] } } @@ -105,7 +108,7 @@ "center": true, "theme": "Dark", "transparent": true, - "userAgent": "Oyasumi/1.2.2 (https://github.com/Raphiiko/Oyasumi)" + "userAgent": "Oyasumi/1.3.0 (https://github.com/Raphiiko/Oyasumi)" }, { "width": 500, @@ -117,7 +120,7 @@ "center": true, "theme": "Dark", "transparent": true, - "userAgent": "Oyasumi/1.2.2 (https://github.com/Raphiiko/Oyasumi)" + "userAgent": "Oyasumi/1.3.0 (https://github.com/Raphiiko/Oyasumi)" } ] } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2d9e1eda..fd5e2f10 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -10,6 +10,9 @@ import { SleepDetectionViewComponent } from './views/dashboard-view/views/sleep- import { GpuAutomationsViewComponent } from './views/dashboard-view/views/gpu-automations-view/gpu-automations-view.component'; import { OscAutomationsViewComponent } from './views/dashboard-view/views/osc-automations-view/osc-automations-view.component'; import { StatusAutomationsViewComponent } from './views/dashboard-view/views/status-automations-view/status-automations-view.component'; +import { + AutoInviteRequestAcceptViewComponent +} from './views/dashboard-view/views/auto-invite-request-accept-view/auto-invite-request-accept-view.component'; const routes: Routes = [ { @@ -41,6 +44,10 @@ const routes: Routes = [ path: 'statusAutomations', component: StatusAutomationsViewComponent, }, + { + path: 'autoInviteRequestAccept', + component: AutoInviteRequestAcceptViewComponent, + }, { path: 'settings', component: SettingsViewComponent, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 315f0662..b7ff5edf 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -74,6 +74,12 @@ import { VRChatLogService } from './services/vrchat-log.service'; import { StatusChangeForPlayerCountAutomationService } from './services/status-automations/status-change-for-player-count-automation.service'; import { MainStatusBarComponent } from './components/main-status-bar/main-status-bar.component'; import { OscControlService } from './services/osc-control.service'; +import { AutoInviteRequestAcceptViewComponent } from './views/dashboard-view/views/auto-invite-request-accept-view/auto-invite-request-accept-view.component'; +import { FriendSelectionModalComponent } from './components/friend-selection-modal/friend-selection-modal.component'; +import { CachedValue } from './utils/cached-value'; +import { ImageCacheService } from './services/image-cache.service'; +import { ImageCachePipe } from './pipes/image-cache.pipe'; +import { InviteAutomationsService } from './services/invite-automations.service'; export function createTranslateLoader(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -102,6 +108,7 @@ export function createTranslateLoader(http: HttpClient) { OscAutomationsViewComponent, SelectBoxComponent, TStringTranslatePipePipe, + ImageCachePipe, OscScriptButtonComponent, OscScriptModalComponent, OscScriptCodeEditorComponent, @@ -119,6 +126,8 @@ export function createTranslateLoader(http: HttpClient) { StatusAutomationsViewComponent, SleepingAnimationPresetModalComponent, MainStatusBarComponent, + AutoInviteRequestAcceptViewComponent, + FriendSelectionModalComponent, ], imports: [ CommonModule, @@ -168,6 +177,7 @@ export class AppModule { private modalService: SimpleModalService, private vrchatService: VRChatService, private vrchatLogService: VRChatLogService, + private imageCacheService: ImageCacheService, // GPU automations private gpuAutomations: GpuAutomationsService, // Sleep mode automations @@ -182,12 +192,16 @@ export class AppModule { // OSC automations private sleepingAnimationsAutomationService: SleepingAnimationsAutomationService, // Status automations - private statusChangeForPlayerCountAutomationService: StatusChangeForPlayerCountAutomationService + private statusChangeForPlayerCountAutomationService: StatusChangeForPlayerCountAutomationService, + // Invite automations + private inviteAutomationsService: InviteAutomationsService ) { this.init(); } async init() { + // Clean cache + await CachedValue.cleanCache(); // Initialize app settings await this.appSettingsService.init(); // Initialize telemetry and updates @@ -201,6 +215,7 @@ export class AppModule { this.sleepService.init(), this.vrchatService.init(), this.vrchatLogService.init(), + this.imageCacheService.init(), ]); // Initialize GPU control services await this.sidecarService.init().then(async () => { @@ -223,6 +238,8 @@ export class AppModule { this.sleepingAnimationsAutomationService.init(), // Status automations this.statusChangeForPlayerCountAutomationService.init(), + // Invite automations + this.inviteAutomationsService.init(), ]); // Language selection modal this.appSettingsService.loadedDefaults diff --git a/src/app/components/dashboard-navbar/dashboard-navbar.component.html b/src/app/components/dashboard-navbar/dashboard-navbar.component.html index 8633524f..226f8dc3 100644 --- a/src/app/components/dashboard-navbar/dashboard-navbar.component.html +++ b/src/app/components/dashboard-navbar/dashboard-navbar.component.html @@ -53,6 +53,13 @@ + +