forked from rojo-rbx/rojo
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e17771a6a5 | ||
|
|
bac30ae78b | ||
|
|
c0219922b2 | ||
|
|
b5ed952d5c | ||
|
|
7994bc4909 | ||
|
|
b88d34c639 | ||
|
|
96cb1ee3fd | ||
|
|
003abe86bb | ||
|
|
6ec411a618 | ||
|
|
c7c0903804 | ||
|
|
cdc972a5ce | ||
|
|
17de912608 | ||
|
|
9876508887 | ||
|
|
72d62220e8 | ||
|
|
46ad337fa5 | ||
|
|
7a3ba7721f | ||
|
|
e0198e626b | ||
|
|
142705f386 | ||
|
|
4cb49c7825 | ||
|
|
05adb82dda | ||
|
|
faf7671799 | ||
|
|
d64db329dd | ||
|
|
e34d2339ad | ||
|
|
d196c5091c | ||
|
|
3e83f92532 | ||
|
|
41d7aaf323 | ||
|
|
e110f3726a | ||
|
|
eb5c897ac0 | ||
|
|
e864cf0c7d | ||
|
|
565c12405e | ||
|
|
2a6a8b42a6 | ||
|
|
5cb4cc0d1d | ||
|
|
62eb4f026f | ||
|
|
411d1a89c1 | ||
|
|
6ae0bf366a | ||
|
|
178cdc9dfa | ||
|
|
5bf1f86886 | ||
|
|
e482aba030 | ||
|
|
535e4d42bb | ||
|
|
54398d4c4b |
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -16,12 +16,10 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
rust_version: [stable, 1.57.0]
|
||||
rust_version: [stable, 1.69.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -30,6 +28,17 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.3.0
|
||||
with:
|
||||
version: 'v0.2.7'
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
cd plugin
|
||||
wally install
|
||||
cd ..
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
|
||||
@@ -42,8 +51,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -52,6 +59,17 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.3.0
|
||||
with:
|
||||
version: 'v0.2.7'
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
cd plugin
|
||||
wally install
|
||||
cd ..
|
||||
|
||||
- name: Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
|
||||
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
@@ -28,13 +28,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Foreman
|
||||
uses: Roblox/setup-foreman@v1
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trust-check: false
|
||||
version: 'v0.2.6'
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
cd plugin
|
||||
wally install
|
||||
cd ..
|
||||
|
||||
- name: Build Plugin
|
||||
run: rojo build plugin --output Rojo.rbxm
|
||||
@@ -61,25 +67,21 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||
#
|
||||
# FIXME: After the Rojo VS Code extension updates, add architecture
|
||||
# names to each of these releases. We'll rename win64 to windows and add
|
||||
# -x86_64 to each release.
|
||||
include:
|
||||
- host: linux
|
||||
os: ubuntu-18.04
|
||||
os: ubuntu-20.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
label: linux
|
||||
label: linux-x86_64
|
||||
|
||||
- host: windows
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
label: win64
|
||||
label: windows-x86_64
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
label: macos
|
||||
label: macos-x86_64
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
@@ -92,8 +94,6 @@ jobs:
|
||||
BIN: rojo
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Get Version from Tag
|
||||
shell: bash
|
||||
@@ -110,6 +110,20 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trust-check: false
|
||||
version: 'v0.2.6'
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
cd plugin
|
||||
wally install
|
||||
cd ..
|
||||
shell: bash
|
||||
|
||||
- name: Build Release
|
||||
run: cargo build --release --locked --verbose
|
||||
env:
|
||||
@@ -150,4 +164,4 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
||||
path: release.zip
|
||||
path: release.zip
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,6 +13,9 @@
|
||||
# Test places for the Roblox Studio Plugin
|
||||
/plugin/*.rbxlx
|
||||
|
||||
# Packages for the Roblox Studio Plugin
|
||||
/plugin/*Packages
|
||||
|
||||
# Roblox Studio holds 'lock' files on places
|
||||
*.rbxl.lock
|
||||
*.rbxlx.lock
|
||||
|
||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -1,15 +0,0 @@
|
||||
[submodule "plugin/modules/roact"]
|
||||
path = plugin/modules/roact
|
||||
url = https://github.com/Roblox/roact.git
|
||||
[submodule "plugin/modules/testez"]
|
||||
path = plugin/modules/testez
|
||||
url = https://github.com/Roblox/testez.git
|
||||
[submodule "plugin/modules/promise"]
|
||||
path = plugin/modules/promise
|
||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||
[submodule "plugin/modules/t"]
|
||||
path = plugin/modules/t
|
||||
url = https://github.com/osyrisrblx/t.git
|
||||
[submodule "plugin/modules/flipper"]
|
||||
path = plugin/modules/flipper
|
||||
url = https://github.com/Reselim/Flipper
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -2,6 +2,50 @@
|
||||
|
||||
## Unreleased Changes
|
||||
|
||||
## [7.3.0] - April 22, 2023
|
||||
* Added `$attributes` to project format. ([#574])
|
||||
* Added `--watch` flag to `rojo sourcemap`. ([#602])
|
||||
* Added support for `init.csv` files. ([#594])
|
||||
* Added real-time sync status to the Studio plugin. ([#569])
|
||||
* Added support for copying error messages to the clipboard. ([#614])
|
||||
* Added sync locking for Team Create. ([#590])
|
||||
* Added support for specifying HTTP or HTTPS protocol in plugin. ([#642])
|
||||
* Added tooltips to buttons in the Studio plugin. ([#637])
|
||||
* Added visual diffs when connecting from the Studio plugin. ([#603])
|
||||
* Host and port are now saved in the Studio plugin. ([#613])
|
||||
* Improved padding on notifications in Studio plugin. ([#589])
|
||||
* Renamed `Common` to `Shared` in the default Rojo project. ([#611])
|
||||
* Reduced the minimum size of the Studio plugin widget. ([#606])
|
||||
* Fixed current directory in `rojo fmt-project`. ([#581])
|
||||
* Fixed errors after a session has already ended. ([#587])
|
||||
* Fixed an uncommon security permission error ([#619])
|
||||
|
||||
[#569]: https://github.com/rojo-rbx/rojo/pull/569
|
||||
[#574]: https://github.com/rojo-rbx/rojo/pull/574
|
||||
[#581]: https://github.com/rojo-rbx/rojo/pull/581
|
||||
[#587]: https://github.com/rojo-rbx/rojo/pull/587
|
||||
[#589]: https://github.com/rojo-rbx/rojo/pull/589
|
||||
[#590]: https://github.com/rojo-rbx/rojo/pull/590
|
||||
[#594]: https://github.com/rojo-rbx/rojo/pull/594
|
||||
[#602]: https://github.com/rojo-rbx/rojo/pull/602
|
||||
[#603]: https://github.com/rojo-rbx/rojo/pull/603
|
||||
[#606]: https://github.com/rojo-rbx/rojo/pull/606
|
||||
[#611]: https://github.com/rojo-rbx/rojo/pull/611
|
||||
[#613]: https://github.com/rojo-rbx/rojo/pull/613
|
||||
[#614]: https://github.com/rojo-rbx/rojo/pull/614
|
||||
[#619]: https://github.com/rojo-rbx/rojo/pull/619
|
||||
[#637]: https://github.com/rojo-rbx/rojo/pull/637
|
||||
[#642]: https://github.com/rojo-rbx/rojo/pull/642
|
||||
[7.3.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.3.0
|
||||
|
||||
## [7.2.1] - July 8, 2022
|
||||
* Fixed notification sound by changing it to a generic sound. ([#566])
|
||||
* Added setting to turn off sound effects. ([#568])
|
||||
|
||||
[#566]: https://github.com/rojo-rbx/rojo/pull/566
|
||||
[#568]: https://github.com/rojo-rbx/rojo/pull/568
|
||||
[7.2.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.1
|
||||
|
||||
## [7.2.0] - June 29, 2022
|
||||
* Added support for `.luau` files. ([#552])
|
||||
* Added support for live syncing Attributes and Tags. ([#553])
|
||||
|
||||
1244
Cargo.lock
generated
1244
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.2.0"
|
||||
rust-version = "1.57.0"
|
||||
version = "7.3.0"
|
||||
rust-version = "1.68.2"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
@@ -51,11 +51,11 @@ memofs = { version = "0.2.0", path = "crates/memofs" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = "0.6.5"
|
||||
rbx_binary = "0.7.0"
|
||||
rbx_dom_weak = "2.4.0"
|
||||
rbx_reflection = "4.2.0"
|
||||
rbx_reflection_database = "0.2.2"
|
||||
rbx_xml = "0.12.3"
|
||||
rbx_reflection_database = "0.2.6"
|
||||
rbx_xml = "0.13.0"
|
||||
|
||||
anyhow = "1.0.44"
|
||||
backtrace = "0.3.61"
|
||||
@@ -73,7 +73,7 @@ log = "0.4.14"
|
||||
maplit = "1.0.2"
|
||||
notify = "4.0.17"
|
||||
opener = "0.5.0"
|
||||
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
||||
reqwest = { version = "0.11.10", features = ["blocking", "json", "native-tls-vendored"] }
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||
@@ -102,7 +102,7 @@ maplit = "1.0.2"
|
||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||
|
||||
criterion = "0.3.5"
|
||||
insta = { version = "1.8.0", features = ["redactions"] }
|
||||
insta = { version = "1.8.0", features = ["redactions", "yaml"] }
|
||||
paste = "1.0.5"
|
||||
pretty_assertions = "1.2.1"
|
||||
serde_yaml = "0.8.21"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a>
|
||||
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a>
|
||||
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a>
|
||||
<a href="https://www.patreon.com/lpghatguy"><img src="https://img.shields.io/badge/sponsor-patreon-red" alt="Patreon" /></a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -40,7 +41,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.57.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
Rojo supports Rust 1.58.1 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
5
aftman.toml
Normal file
5
aftman.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[tools]
|
||||
wally = "UpliftGames/wally@0.3.1"
|
||||
rojo = "rojo-rbx/rojo@7.2.1"
|
||||
selene = "Kampfkarren/selene@0.20.0"
|
||||
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
||||
@@ -4,7 +4,7 @@
|
||||
"$className": "DataModel",
|
||||
|
||||
"ReplicatedStorage": {
|
||||
"Common": {
|
||||
"Shared": {
|
||||
"$path": "src/shared"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
use librojo::cli::{build, BuildCommand};
|
||||
use librojo::cli::BuildCommand;
|
||||
|
||||
pub fn benchmark_small_place(c: &mut Criterion) {
|
||||
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
||||
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
||||
group.bench_function("build", |b| {
|
||||
b.iter_batched(
|
||||
|| place_setup(path),
|
||||
|(_dir, options)| build(options).unwrap(),
|
||||
|(_dir, options)| options.run().unwrap(),
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
|
||||
17
build.rs
17
build.rs
@@ -43,8 +43,6 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||
|
||||
let plugin_modules = plugin_root.join("modules");
|
||||
|
||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||
@@ -52,20 +50,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||
"modules" => VfsSnapshot::dir(hashmap! {
|
||||
"roact" => VfsSnapshot::dir(hashmap! {
|
||||
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
|
||||
}),
|
||||
"promise" => VfsSnapshot::dir(hashmap! {
|
||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
|
||||
}),
|
||||
"t" => VfsSnapshot::dir(hashmap! {
|
||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
|
||||
}),
|
||||
"flipper" => VfsSnapshot::dir(hashmap! {
|
||||
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
|
||||
}),
|
||||
}),
|
||||
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
||||
});
|
||||
|
||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[tools]
|
||||
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
|
||||
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
|
||||
selene = { source = "Kampfkarren/selene", version = "0.18.2" }
|
||||
@@ -1,33 +1,25 @@
|
||||
{
|
||||
"name": "Rojo",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"Plugin": {
|
||||
"$path": "src"
|
||||
},
|
||||
"Log": {
|
||||
"$path": "log"
|
||||
},
|
||||
"Http": {
|
||||
"$path": "http"
|
||||
},
|
||||
"Fmt": {
|
||||
"$path": "fmt"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "rbx_dom_lua"
|
||||
},
|
||||
"Roact": {
|
||||
"$path": "modules/roact/src"
|
||||
},
|
||||
"Promise": {
|
||||
"$path": "modules/promise/lib"
|
||||
},
|
||||
"t": {
|
||||
"$path": "modules/t/lib"
|
||||
},
|
||||
"Flipper": {
|
||||
"$path": "modules/flipper/src"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "Rojo",
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"Plugin": {
|
||||
"$path": "src"
|
||||
},
|
||||
"Packages": {
|
||||
"$path": "Packages",
|
||||
|
||||
"Log": {
|
||||
"$path": "log"
|
||||
},
|
||||
"Http": {
|
||||
"$path": "http"
|
||||
},
|
||||
"Fmt": {
|
||||
"$path": "fmt"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "rbx_dom_lua"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule plugin/modules/flipper deleted from 4cf7a03cb6
Submodule plugin/modules/promise deleted from 7fb09d103f
Submodule plugin/modules/roact deleted from f7d2f1ce1d
Submodule plugin/modules/t deleted from f643b50682
Submodule plugin/modules/testez deleted from 25d957d4d5
@@ -238,6 +238,23 @@ types = {
|
||||
toPod = serializeFloat,
|
||||
},
|
||||
|
||||
Font = {
|
||||
fromPod = function(pod)
|
||||
return Font.new(
|
||||
pod.family,
|
||||
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
|
||||
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
|
||||
)
|
||||
end,
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
family = roblox.Family,
|
||||
weight = roblox.Weight.Name,
|
||||
style = roblox.Style.Name,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Int32 = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
|
||||
@@ -53,6 +53,11 @@ function PropertyDescriptor:read(instance)
|
||||
end
|
||||
|
||||
if self.scriptability == "Custom" then
|
||||
if customProperties[self.className] == nil then
|
||||
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
|
||||
end
|
||||
|
||||
local interface = customProperties[self.className][self.name]
|
||||
|
||||
return interface.read(instance, self.name)
|
||||
@@ -79,6 +84,11 @@ function PropertyDescriptor:write(instance, value)
|
||||
end
|
||||
|
||||
if self.scriptability == "Custom" then
|
||||
if customProperties[self.className] == nil then
|
||||
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
|
||||
end
|
||||
|
||||
local interface = customProperties[self.className][self.name]
|
||||
|
||||
return interface.write(instance, self.name, value)
|
||||
|
||||
@@ -207,6 +207,17 @@
|
||||
},
|
||||
"ty": "Float64"
|
||||
},
|
||||
"Font": {
|
||||
"value": {
|
||||
"Font": {
|
||||
"family": "rbxasset://fonts/families/SourceSansPro.json",
|
||||
"weight": "Regular",
|
||||
"style": "Normal",
|
||||
"cachedFaceId": null
|
||||
}
|
||||
},
|
||||
"ty": "Font"
|
||||
},
|
||||
"Int32": {
|
||||
"value": {
|
||||
"Int32": 6014
|
||||
|
||||
@@ -61,4 +61,14 @@ return {
|
||||
end,
|
||||
},
|
||||
},
|
||||
Model = {
|
||||
Scale = {
|
||||
read = function(instance, _, _)
|
||||
return true, instance:GetScale()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
return true, instance:ScaleTo(value)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,11 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.TestEZ)
|
||||
local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
|
||||
|
||||
local Rojo = ReplicatedStorage.Rojo
|
||||
|
||||
local DevSettings = require(Rojo.Plugin.DevSettings)
|
||||
|
||||
local setDevSettings = not DevSettings:hasChangedValues()
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:createTestSettings()
|
||||
end
|
||||
local Settings = require(Rojo.Plugin.Settings)
|
||||
Settings:set("logLevel", "Trace")
|
||||
Settings:set("typecheckingEnabled", true)
|
||||
|
||||
require(Rojo.Plugin.runTests)(TestEZ)
|
||||
|
||||
if setDevSettings then
|
||||
DevSettings:resetValues()
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
local Http = require(script.Parent.Parent.Http)
|
||||
local Log = require(script.Parent.Parent.Log)
|
||||
local Promise = require(script.Parent.Parent.Promise)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Http = require(Packages.Http)
|
||||
local Log = require(Packages.Log)
|
||||
local Promise = require(Packages.Promise)
|
||||
|
||||
local Config = require(script.Parent.Config)
|
||||
local Types = require(script.Parent.Types)
|
||||
@@ -85,7 +86,7 @@ local ApiContext = {}
|
||||
ApiContext.__index = ApiContext
|
||||
|
||||
function ApiContext.new(baseUrl)
|
||||
assert(type(baseUrl) == "string")
|
||||
assert(type(baseUrl) == "string", "baseUrl must be a string")
|
||||
|
||||
local self = {
|
||||
__baseUrl = baseUrl,
|
||||
@@ -248,4 +249,4 @@ function ApiContext:open(id)
|
||||
end)
|
||||
end
|
||||
|
||||
return ApiContext
|
||||
return ApiContext
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
@@ -23,8 +24,10 @@ local function BorderedContainer(props)
|
||||
layoutOrder = props.layoutOrder,
|
||||
}, {
|
||||
Content = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Size = UDim2.new(1, -2, 1, -2),
|
||||
Position = UDim2.new(0, 1, 0, 1),
|
||||
BackgroundTransparency = 1,
|
||||
ZIndex = 2,
|
||||
}, props[Roact.Children]),
|
||||
|
||||
Border = e(SlicedImage, {
|
||||
@@ -38,4 +41,4 @@ local function BorderedContainer(props)
|
||||
end)
|
||||
end
|
||||
|
||||
return BorderedContainer
|
||||
return BorderedContainer
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Flipper = require(Rojo.Flipper)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
local Tooltip = require(script.Parent.Tooltip)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -51,6 +53,10 @@ function Checkbox:render()
|
||||
|
||||
[Roact.Event.Activated] = self.props.onClick,
|
||||
}, {
|
||||
StateTip = e(Tooltip.Trigger, {
|
||||
text = if self.props.active then "Enabled" else "Disabled",
|
||||
}),
|
||||
|
||||
Active = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = theme.Active.BackgroundColor,
|
||||
@@ -93,4 +99,4 @@ function Checkbox:render()
|
||||
end)
|
||||
end
|
||||
|
||||
return Checkbox
|
||||
return Checkbox
|
||||
|
||||
169
plugin/src/App/Components/Dropdown.lua
Normal file
169
plugin/src/App/Components/Dropdown.lua
Normal file
@@ -0,0 +1,169 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Dropdown = Roact.Component:extend("Dropdown")
|
||||
|
||||
function Dropdown:init()
|
||||
self.openMotor = Flipper.SingleMotor.new(0)
|
||||
self.openBinding = bindingUtil.fromMotor(self.openMotor)
|
||||
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
self:setState({
|
||||
open = false,
|
||||
})
|
||||
end
|
||||
|
||||
function Dropdown:didUpdate()
|
||||
self.openMotor:setGoal(
|
||||
Flipper.Spring.new(self.state.open and 1 or 0, {
|
||||
frequency = 6,
|
||||
dampingRatio = 1.1,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
function Dropdown:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Dropdown
|
||||
|
||||
local optionButtons = {}
|
||||
local width = -1
|
||||
for i, option in self.props.options do
|
||||
local text = tostring(option or "")
|
||||
local textSize = TextService:GetTextSize(
|
||||
text, 15, Enum.Font.GothamMedium,
|
||||
Vector2.new(math.huge, 20)
|
||||
)
|
||||
if textSize.X > width then
|
||||
width = textSize.X
|
||||
end
|
||||
|
||||
optionButtons[text] = e("TextButton", {
|
||||
Text = text,
|
||||
LayoutOrder = i,
|
||||
Size = UDim2.new(1, 0, 0, 24),
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
BackgroundTransparency = self.props.transparency,
|
||||
BorderSizePixel = 0,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextSize = 15,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
open = false,
|
||||
})
|
||||
self.props.onClick(option)
|
||||
end,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 6),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return e("ImageButton", {
|
||||
Size = UDim2.new(0, width+50, 0, 28),
|
||||
Position = self.props.position,
|
||||
AnchorPoint = self.props.anchorPoint,
|
||||
LayoutOrder = self.props.layoutOrder,
|
||||
ZIndex = self.props.zIndex,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
self:setState({
|
||||
open = not self.state.open,
|
||||
})
|
||||
end,
|
||||
}, {
|
||||
Border = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
DropArrow = e("ImageLabel", {
|
||||
Image = Assets.Images.Dropdown.Arrow,
|
||||
ImageColor3 = self.openBinding:map(function(a)
|
||||
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
|
||||
end),
|
||||
ImageTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(0, 18, 0, 18),
|
||||
Position = UDim2.new(1, -6, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(1, 0.5),
|
||||
Rotation = self.openBinding:map(function(a)
|
||||
return a * 180
|
||||
end),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
Active = e("TextLabel", {
|
||||
Size = UDim2.new(1, -30, 1, 0),
|
||||
Position = UDim2.new(0, 6, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Text = self.props.active,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
}),
|
||||
}),
|
||||
Options = if self.state.open then e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = theme.BackgroundColor,
|
||||
position = UDim2.new(1, 0, 1, 3),
|
||||
size = self.openBinding:map(function(a)
|
||||
return UDim2.new(1, 0, a*math.min(3, #self.props.options), 0)
|
||||
end),
|
||||
anchorPoint = Vector2.new(1, 0),
|
||||
}, {
|
||||
Border = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}),
|
||||
ScrollingFrame = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, -4, 1, -4),
|
||||
position = UDim2.new(0, 2, 0, 2),
|
||||
transparency = self.props.transparency,
|
||||
contentSize = self.contentSize,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 0),
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
Roact.createFragment(optionButtons),
|
||||
}),
|
||||
}) else nil,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return Dropdown
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
@@ -52,4 +53,4 @@ local function Header(props)
|
||||
end)
|
||||
end
|
||||
|
||||
return Header
|
||||
return Header
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Flipper = require(Rojo.Flipper)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
@@ -29,6 +30,7 @@ function IconButton:render()
|
||||
Position = self.props.position,
|
||||
AnchorPoint = self.props.anchorPoint,
|
||||
|
||||
Visible = self.props.visible,
|
||||
LayoutOrder = self.props.layoutOrder,
|
||||
ZIndex = self.props.zIndex,
|
||||
BackgroundTransparency = 1,
|
||||
@@ -73,7 +75,9 @@ function IconButton:render()
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Children = Roact.createFragment(self.props[Roact.Children]),
|
||||
})
|
||||
end
|
||||
|
||||
return IconButton
|
||||
return IconButton
|
||||
|
||||
181
plugin/src/App/Components/PatchVisualizer/ChangeList.lua
Normal file
181
plugin/src/App/Components/PatchVisualizer/ChangeList.lua
Normal file
@@ -0,0 +1,181 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
local DisplayValue = require(script.Parent.DisplayValue)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local ChangeList = Roact.Component:extend("ChangeList")
|
||||
|
||||
function ChangeList:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
end
|
||||
|
||||
function ChangeList:render()
|
||||
return Theme.with(function(theme)
|
||||
local props = self.props
|
||||
local changes = props.changes
|
||||
|
||||
-- Color alternating rows for readability
|
||||
local rowTransparency = props.transparency:map(function(t)
|
||||
return 0.93 + (0.07 * t)
|
||||
end)
|
||||
|
||||
local columnVisibility = props.columnVisibility
|
||||
|
||||
local rows = {}
|
||||
local pad = {
|
||||
PaddingLeft = UDim.new(0, 5),
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}
|
||||
|
||||
local headers = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = rowTransparency,
|
||||
BackgroundColor3 = theme.Diff.Row,
|
||||
LayoutOrder = 0,
|
||||
}, {
|
||||
Padding = e("UIPadding", pad),
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
A = e("TextLabel", {
|
||||
Visible = columnVisibility[1],
|
||||
Text = tostring(changes[1][1]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.3, 0, 1, 0),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
B = e("TextLabel", {
|
||||
Visible = columnVisibility[2],
|
||||
Text = tostring(changes[1][2]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 2,
|
||||
}),
|
||||
C = e("TextLabel", {
|
||||
Visible = columnVisibility[3],
|
||||
Text = tostring(changes[1][3]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 3,
|
||||
}),
|
||||
})
|
||||
|
||||
for row, values in changes do
|
||||
if row == 1 then
|
||||
continue -- Skip headers, already handled above
|
||||
end
|
||||
|
||||
rows[row] = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||
BackgroundColor3 = theme.Diff.Row,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = row,
|
||||
}, {
|
||||
Padding = e("UIPadding", pad),
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
A = e("TextLabel", {
|
||||
Visible = columnVisibility[1],
|
||||
Text = tostring(values[1]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.3, 0, 1, 0),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
B = e(
|
||||
"Frame",
|
||||
{
|
||||
Visible = columnVisibility[2],
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 2,
|
||||
},
|
||||
e(DisplayValue, {
|
||||
value = values[2],
|
||||
transparency = props.transparency,
|
||||
})
|
||||
),
|
||||
C = e(
|
||||
"Frame",
|
||||
{
|
||||
Visible = columnVisibility[3],
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 3,
|
||||
},
|
||||
e(DisplayValue, {
|
||||
value = values[3],
|
||||
transparency = props.transparency,
|
||||
})
|
||||
),
|
||||
})
|
||||
end
|
||||
|
||||
table.insert(
|
||||
rows,
|
||||
e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
})
|
||||
)
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Headers = headers,
|
||||
Values = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, -30),
|
||||
position = UDim2.new(0, 0, 0, 30),
|
||||
contentSize = self.contentSize,
|
||||
transparency = props.transparency,
|
||||
}, rows),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return ChangeList
|
||||
107
plugin/src/App/Components/PatchVisualizer/DisplayValue.lua
Normal file
107
plugin/src/App/Components/PatchVisualizer/DisplayValue.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local function DisplayValue(props)
|
||||
return Theme.with(function(theme)
|
||||
local t = typeof(props.value)
|
||||
if t == "Color3" then
|
||||
-- Colors get a blot that shows the color
|
||||
return Roact.createFragment({
|
||||
Blot = e("Frame", {
|
||||
BackgroundTransparency = props.transparency,
|
||||
BackgroundColor3 = props.value,
|
||||
Size = UDim2.new(0, 20, 0, 20),
|
||||
Position = UDim2.new(0, 0, 0.5, 0),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
}, {
|
||||
Corner = e("UICorner", {
|
||||
CornerRadius = UDim.new(0, 4),
|
||||
}),
|
||||
Stroke = e("UIStroke", {
|
||||
Color = theme.BorderedContainer.BorderColor,
|
||||
Transparency = props.transparency,
|
||||
}),
|
||||
}),
|
||||
Label = e("TextLabel", {
|
||||
Text = string.format("%d,%d,%d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(1, -25, 1, 0),
|
||||
Position = UDim2.new(0, 25, 0, 0),
|
||||
}),
|
||||
})
|
||||
|
||||
elseif t == "table" then
|
||||
-- Showing a memory address for tables is useless, so we want to show the best we can
|
||||
local textRepresentation = nil
|
||||
|
||||
local meta = getmetatable(props.value)
|
||||
if meta and meta.__tostring then
|
||||
-- If the table has a tostring metamethod, use that
|
||||
textRepresentation = tostring(props.value)
|
||||
elseif next(props.value) == nil then
|
||||
-- If it's empty, show empty braces
|
||||
textRepresentation = "{}"
|
||||
else
|
||||
-- If it has children, list them out
|
||||
local out, i = {}, 0
|
||||
for k, v in pairs(props.value) do
|
||||
i += 1
|
||||
|
||||
-- Wrap strings in quotes
|
||||
if type(k) == "string" then
|
||||
k = "\"" .. k .. "\""
|
||||
end
|
||||
if type(v) == "string" then
|
||||
v = "\"" .. v .. "\""
|
||||
end
|
||||
|
||||
out[i] = string.format("[%s] = %s", tostring(k), tostring(v))
|
||||
end
|
||||
textRepresentation = "{ " .. table.concat(out, ", ") .. " }"
|
||||
end
|
||||
|
||||
return e("TextLabel", {
|
||||
Text = textRepresentation,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
})
|
||||
end
|
||||
|
||||
-- TODO: Maybe add visualizations to other datatypes?
|
||||
-- Or special text handling tostring for some?
|
||||
-- Will add as needed, let's see what cases arise.
|
||||
|
||||
return e("TextLabel", {
|
||||
Text = string.gsub(tostring(props.value), "%s", " "),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return DisplayValue
|
||||
180
plugin/src/App/Components/PatchVisualizer/DomLabel.lua
Normal file
180
plugin/src/App/Components/PatchVisualizer/DomLabel.lua
Normal file
@@ -0,0 +1,180 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local ChangeList = require(script.Parent.ChangeList)
|
||||
|
||||
local Expansion = Roact.Component:extend("Expansion")
|
||||
|
||||
function Expansion:render()
|
||||
local props = self.props
|
||||
|
||||
if not props.rendered then
|
||||
return nil
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -props.indent, 1, -30),
|
||||
Position = UDim2.new(0, props.indent, 0, 30),
|
||||
}, {
|
||||
ChangeList = e(ChangeList, {
|
||||
changes = props.changeList,
|
||||
transparency = props.transparency,
|
||||
columnVisibility = props.columnVisibility,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local DomLabel = Roact.Component:extend("DomLabel")
|
||||
|
||||
function DomLabel:init()
|
||||
self.maxElementHeight = 0
|
||||
if self.props.changeList then
|
||||
self.maxElementHeight = math.clamp(#self.props.changeList * 30, 30, 30 * 6)
|
||||
end
|
||||
|
||||
local initHeight = self.props.elementHeight:getValue()
|
||||
self.expanded = initHeight > 30
|
||||
|
||||
self.motor = Flipper.SingleMotor.new(initHeight)
|
||||
self.binding = bindingUtil.fromMotor(self.motor)
|
||||
|
||||
self:setState({
|
||||
renderExpansion = self.expanded,
|
||||
})
|
||||
self.motor:onStep(function(value)
|
||||
local renderExpansion = value > 30
|
||||
|
||||
self.props.setElementHeight(value)
|
||||
if self.props.updateEvent then
|
||||
self.props.updateEvent:Fire()
|
||||
end
|
||||
|
||||
self:setState(function(state)
|
||||
if state.renderExpansion == renderExpansion then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
renderExpansion = renderExpansion,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function DomLabel:render()
|
||||
local props = self.props
|
||||
|
||||
return Theme.with(function(theme)
|
||||
local iconProps = StudioService:GetClassIcon(props.className)
|
||||
local indent = (props.depth or 0) * 20 + 25
|
||||
|
||||
-- Line guides help indent depth remain readable
|
||||
local lineGuides = {}
|
||||
for i = 1, props.depth or 0 do
|
||||
table.insert(
|
||||
lineGuides,
|
||||
e("Frame", {
|
||||
Name = "Line_" .. i,
|
||||
Size = UDim2.new(0, 2, 1, 2),
|
||||
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = props.transparency,
|
||||
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
Name = "Change",
|
||||
ClipsDescendants = true,
|
||||
BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = props.patchType and props.transparency or 1,
|
||||
Size = self.binding:map(function(expand)
|
||||
return UDim2.new(1, 0, 0, expand)
|
||||
end),
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 10),
|
||||
PaddingRight = UDim.new(0, 10),
|
||||
}),
|
||||
ExpandButton = if props.changeList
|
||||
then e("TextButton", {
|
||||
BackgroundTransparency = 1,
|
||||
Text = "",
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
[Roact.Event.Activated] = function()
|
||||
self.expanded = not self.expanded
|
||||
self.motor:setGoal(Flipper.Spring.new((self.expanded and self.maxElementHeight or 0) + 30, {
|
||||
frequency = 5,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
end,
|
||||
})
|
||||
else nil,
|
||||
Expansion = if props.changeList
|
||||
then e(Expansion, {
|
||||
rendered = self.state.renderExpansion,
|
||||
indent = indent,
|
||||
transparency = props.transparency,
|
||||
changeList = props.changeList,
|
||||
columnVisibility = props.columnVisibility,
|
||||
})
|
||||
else nil,
|
||||
DiffIcon = if props.patchType
|
||||
then e("ImageLabel", {
|
||||
Image = Assets.Images.Diff[props.patchType],
|
||||
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
||||
ImageTransparency = props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 20, 0, 20),
|
||||
Position = UDim2.new(0, 0, 0, 15),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
})
|
||||
else nil,
|
||||
ClassIcon = e("ImageLabel", {
|
||||
Image = iconProps.Image,
|
||||
ImageTransparency = props.transparency,
|
||||
ImageRectOffset = iconProps.ImageRectOffset,
|
||||
ImageRectSize = iconProps.ImageRectSize,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 20, 0, 20),
|
||||
Position = UDim2.new(0, indent, 0, 15),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
}),
|
||||
InstanceName = e("TextLabel", {
|
||||
Text = props.name .. (props.hint and string.format(
|
||||
' <font color="#%s">%s</font>',
|
||||
theme.AddressEntry.PlaceholderColor:ToHex(),
|
||||
props.hint
|
||||
) or ""),
|
||||
RichText = true,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(1, -indent - 50, 0, 30),
|
||||
Position = UDim2.new(0, indent + 30, 0, 0),
|
||||
}),
|
||||
LineGuides = e("Folder", nil, lineGuides),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return DomLabel
|
||||
402
plugin/src/App/Components/PatchVisualizer/init.lua
Normal file
402
plugin/src/App/Components/PatchVisualizer/init.lua
Normal file
@@ -0,0 +1,402 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local PatchSet = require(Plugin.PatchSet)
|
||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local function alphabeticalNext(t, state)
|
||||
-- Equivalent of the next function, but returns the keys in the alphabetic
|
||||
-- order of node names. We use a temporary ordered key table that is stored in the
|
||||
-- table being iterated.
|
||||
|
||||
local key = nil
|
||||
if state == nil then
|
||||
-- First iteration, generate the index
|
||||
local orderedIndex, i = table.create(5), 0
|
||||
for k in t do
|
||||
i += 1
|
||||
orderedIndex[i] = k
|
||||
end
|
||||
table.sort(orderedIndex, function(a, b)
|
||||
local nodeA, nodeB = t[a], t[b]
|
||||
return (nodeA.name or "") < (nodeB.name or "")
|
||||
end)
|
||||
|
||||
t.__orderedIndex = orderedIndex
|
||||
key = orderedIndex[1]
|
||||
else
|
||||
-- Fetch the next value
|
||||
for i, orderedState in t.__orderedIndex do
|
||||
if orderedState == state then
|
||||
key = t.__orderedIndex[i + 1]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if key then
|
||||
return key, t[key]
|
||||
end
|
||||
|
||||
-- No more value to return, cleanup
|
||||
t.__orderedIndex = nil
|
||||
return
|
||||
end
|
||||
|
||||
local function alphabeticalPairs(t)
|
||||
-- Equivalent of the pairs() iterator, but sorted
|
||||
return alphabeticalNext, t, nil
|
||||
end
|
||||
|
||||
local function Tree()
|
||||
local tree = {
|
||||
idToNode = {},
|
||||
ROOT = {
|
||||
className = "DataModel",
|
||||
name = "ROOT",
|
||||
children = {},
|
||||
},
|
||||
}
|
||||
-- Add ROOT to idToNode or it won't be found by getNode since that searches *within* ROOT
|
||||
tree.idToNode["ROOT"] = tree.ROOT
|
||||
|
||||
function tree:getNode(id, target)
|
||||
if self.idToNode[id] then
|
||||
return self.idToNode[id]
|
||||
end
|
||||
|
||||
for nodeId, node in target or tree.ROOT.children do
|
||||
if nodeId == id then
|
||||
self.idToNode[id] = node
|
||||
return node
|
||||
end
|
||||
local descendant = self:getNode(id, node.children)
|
||||
if descendant then
|
||||
return descendant
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function tree:addNode(parent, props)
|
||||
parent = parent or "ROOT"
|
||||
|
||||
local node = self:getNode(props.id)
|
||||
if node then
|
||||
for k, v in props do
|
||||
node[k] = v
|
||||
end
|
||||
return node
|
||||
end
|
||||
|
||||
node = table.clone(props)
|
||||
node.children = {}
|
||||
|
||||
local parentNode = self:getNode(parent)
|
||||
if not parentNode then
|
||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
||||
return
|
||||
end
|
||||
|
||||
parentNode.children[node.id] = node
|
||||
self.idToNode[node.id] = node
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
function tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
||||
-- Build nodes for ancestry by going up the tree
|
||||
local previousId = "ROOT"
|
||||
for _, ancestorId in ancestry do
|
||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
||||
if not value then
|
||||
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
||||
continue
|
||||
end
|
||||
self:addNode(previousId, {
|
||||
id = ancestorId,
|
||||
className = value.ClassName,
|
||||
name = value.Name,
|
||||
})
|
||||
previousId = ancestorId
|
||||
end
|
||||
end
|
||||
|
||||
return tree
|
||||
end
|
||||
|
||||
local DomLabel = require(script.DomLabel)
|
||||
|
||||
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
|
||||
|
||||
function PatchVisualizer:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
self.updateEvent = Instance.new("BindableEvent")
|
||||
end
|
||||
|
||||
function PatchVisualizer:willUnmount()
|
||||
self.updateEvent:Destroy()
|
||||
end
|
||||
|
||||
function PatchVisualizer:shouldUpdate(nextProps)
|
||||
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
||||
|
||||
return not PatchSet.isEqual(currentPatch, nextPatch)
|
||||
end
|
||||
|
||||
function PatchVisualizer:buildTree(patch, instanceMap)
|
||||
local tree = Tree()
|
||||
|
||||
for _, change in patch.updated do
|
||||
local instance = instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
continue
|
||||
end
|
||||
|
||||
-- Gather ancestors from existing DOM
|
||||
local ancestry = {}
|
||||
local parentObject = instance.Parent
|
||||
local parentId = instanceMap.fromInstances[parentObject]
|
||||
while parentObject do
|
||||
table.insert(ancestry, 1, parentId)
|
||||
parentObject = parentObject.Parent
|
||||
parentId = instanceMap.fromInstances[parentObject]
|
||||
end
|
||||
|
||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
||||
|
||||
-- Gather detail text
|
||||
local changeList, hint = nil, nil
|
||||
if next(change.changedProperties) or change.changedName then
|
||||
changeList = {}
|
||||
|
||||
local hintBuffer, i = {}, 0
|
||||
local function addProp(prop: string, current: any?, incoming: any?)
|
||||
i += 1
|
||||
hintBuffer[i] = prop
|
||||
changeList[i] = { prop, current, incoming }
|
||||
end
|
||||
|
||||
-- Gather the changes
|
||||
|
||||
if change.changedName then
|
||||
addProp("Name", instance.Name, change.changedName)
|
||||
end
|
||||
|
||||
for prop, incoming in change.changedProperties do
|
||||
local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
|
||||
local currentSuccess, currentValue = getProperty(instance, prop)
|
||||
|
||||
addProp(
|
||||
prop,
|
||||
if currentSuccess then currentValue else "[Error]",
|
||||
if incomingSuccess then incomingValue else next(incoming)
|
||||
)
|
||||
end
|
||||
|
||||
-- Finalize detail values
|
||||
|
||||
-- Trim hint to top 3
|
||||
table.sort(hintBuffer)
|
||||
if #hintBuffer > 3 then
|
||||
hintBuffer = {
|
||||
hintBuffer[1],
|
||||
hintBuffer[2],
|
||||
hintBuffer[3],
|
||||
i - 3 .. " more",
|
||||
}
|
||||
end
|
||||
hint = table.concat(hintBuffer, ", ")
|
||||
|
||||
-- Sort changes and add header
|
||||
table.sort(changeList, function(a, b)
|
||||
return a[1] < b[1]
|
||||
end)
|
||||
table.insert(changeList, 1, { "Property", "Current", "Incoming" })
|
||||
end
|
||||
|
||||
-- Add this node to tree
|
||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||
id = change.id,
|
||||
patchType = "Edit",
|
||||
className = instance.ClassName,
|
||||
name = instance.Name,
|
||||
hint = hint,
|
||||
changeList = changeList,
|
||||
})
|
||||
end
|
||||
|
||||
for _, instance in patch.removed do
|
||||
-- Gather ancestors from existing DOM
|
||||
-- (note that they may have no ID if they're being removed as unknown)
|
||||
local ancestry = {}
|
||||
local parentObject = instance.Parent
|
||||
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
||||
while parentObject do
|
||||
instanceMap:insert(parentId, parentObject)
|
||||
table.insert(ancestry, 1, parentId)
|
||||
parentObject = parentObject.Parent
|
||||
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
||||
end
|
||||
|
||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
||||
|
||||
-- Add this node to tree
|
||||
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
||||
instanceMap:insert(nodeId, instance)
|
||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||
id = nodeId,
|
||||
patchType = "Remove",
|
||||
className = instance.ClassName,
|
||||
name = instance.Name,
|
||||
})
|
||||
end
|
||||
|
||||
for _, change in patch.added do
|
||||
-- Gather ancestors from existing DOM or future additions
|
||||
local ancestry = {}
|
||||
local parentId = change.Parent
|
||||
local parentData = patch.added[parentId]
|
||||
local parentObject = instanceMap.fromIds[parentId]
|
||||
while parentId do
|
||||
table.insert(ancestry, 1, parentId)
|
||||
parentId = nil
|
||||
|
||||
if parentData then
|
||||
parentId = parentData.Parent
|
||||
parentData = patch.added[parentId]
|
||||
parentObject = instanceMap.fromIds[parentId]
|
||||
elseif parentObject then
|
||||
parentObject = parentObject.Parent
|
||||
parentId = instanceMap.fromInstances[parentObject]
|
||||
parentData = patch.added[parentId]
|
||||
end
|
||||
end
|
||||
|
||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
||||
|
||||
-- Gather detail text
|
||||
local changeList, hint = nil, nil
|
||||
if next(change.Properties) then
|
||||
changeList = {}
|
||||
|
||||
local hintBuffer, i = {}, 0
|
||||
for prop, incoming in change.Properties do
|
||||
i += 1
|
||||
hintBuffer[i] = prop
|
||||
|
||||
local success, incomingValue = decodeValue(incoming, instanceMap)
|
||||
if success then
|
||||
table.insert(changeList, { prop, "N/A", incomingValue })
|
||||
else
|
||||
table.insert(changeList, { prop, "N/A", next(incoming) })
|
||||
end
|
||||
end
|
||||
|
||||
-- Finalize detail values
|
||||
|
||||
-- Trim hint to top 3
|
||||
table.sort(hintBuffer)
|
||||
if #hintBuffer > 3 then
|
||||
hintBuffer = {
|
||||
hintBuffer[1],
|
||||
hintBuffer[2],
|
||||
hintBuffer[3],
|
||||
i - 3 .. " more",
|
||||
}
|
||||
end
|
||||
hint = table.concat(hintBuffer, ", ")
|
||||
|
||||
-- Sort changes and add header
|
||||
table.sort(changeList, function(a, b)
|
||||
return a[1] < b[1]
|
||||
end)
|
||||
table.insert(changeList, 1, { "Property", "Current", "Incoming" })
|
||||
end
|
||||
|
||||
-- Add this node to tree
|
||||
tree:addNode(change.Parent, {
|
||||
id = change.Id,
|
||||
patchType = "Add",
|
||||
className = change.ClassName,
|
||||
name = change.Name,
|
||||
hint = hint,
|
||||
changeList = changeList,
|
||||
})
|
||||
end
|
||||
|
||||
return tree
|
||||
end
|
||||
|
||||
function PatchVisualizer:render()
|
||||
local patch = self.props.patch
|
||||
local instanceMap = self.props.instanceMap
|
||||
|
||||
local tree = self:buildTree(patch, instanceMap)
|
||||
|
||||
-- Recusively draw tree
|
||||
local scrollElements, elementHeights = {}, {}
|
||||
local function drawNode(node, depth)
|
||||
local elementHeight, setElementHeight = Roact.createBinding(30)
|
||||
table.insert(elementHeights, elementHeight)
|
||||
table.insert(
|
||||
scrollElements,
|
||||
e(DomLabel, {
|
||||
columnVisibility = self.props.columnVisibility,
|
||||
updateEvent = self.updateEvent,
|
||||
elementHeight = elementHeight,
|
||||
setElementHeight = setElementHeight,
|
||||
patchType = node.patchType,
|
||||
className = node.className,
|
||||
name = node.name,
|
||||
hint = node.hint,
|
||||
changeList = node.changeList,
|
||||
depth = depth,
|
||||
transparency = self.props.transparency,
|
||||
})
|
||||
)
|
||||
|
||||
for _, childNode in alphabeticalPairs(node.children) do
|
||||
drawNode(childNode, depth + 1)
|
||||
end
|
||||
end
|
||||
for _, node in alphabeticalPairs(tree.ROOT.children) do
|
||||
drawNode(node, 0)
|
||||
end
|
||||
|
||||
return e(BorderedContainer, {
|
||||
transparency = self.props.transparency,
|
||||
size = self.props.size,
|
||||
position = self.props.position,
|
||||
layoutOrder = self.props.layoutOrder,
|
||||
}, {
|
||||
VirtualScroller = e(VirtualScroller, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
transparency = self.props.transparency,
|
||||
count = #scrollElements,
|
||||
updateEvent = self.updateEvent.Event,
|
||||
render = function(i)
|
||||
return scrollElements[i]
|
||||
end,
|
||||
getHeightBinding = function(i)
|
||||
return elementHeights[i]
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return PatchVisualizer
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
@@ -39,4 +40,4 @@ local function ScrollingFrame(props)
|
||||
end)
|
||||
end
|
||||
|
||||
return ScrollingFrame
|
||||
return ScrollingFrame
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -26,4 +27,4 @@ local function SlicedImage(props)
|
||||
}, props[Roact.Children])
|
||||
end
|
||||
|
||||
return SlicedImage
|
||||
return SlicedImage
|
||||
|
||||
@@ -2,8 +2,9 @@ local RunService = game:GetService("RunService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
@@ -63,4 +64,4 @@ function Spinner:willUnmount()
|
||||
self.stepper:Disconnect()
|
||||
end
|
||||
|
||||
return Spinner
|
||||
return Spinner
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Dictionary = require(Plugin.Dictionary)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local StudioPluginContext = Roact.createContext(nil)
|
||||
|
||||
return StudioPluginContext
|
||||
return StudioPluginContext
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Dictionary = require(Plugin.Dictionary)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local StudioPluginContext = require(script.Parent.StudioPluginContext)
|
||||
|
||||
@@ -28,8 +30,10 @@ function StudioPluginGui:init()
|
||||
self.props.initDockState,
|
||||
self.props.active,
|
||||
self.props.overridePreviousState,
|
||||
floatingSize.X, floatingSize.Y,
|
||||
minimumSize.X, minimumSize.Y
|
||||
floatingSize.X,
|
||||
floatingSize.Y,
|
||||
minimumSize.X,
|
||||
minimumSize.Y
|
||||
)
|
||||
|
||||
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(self.props.id, dockWidgetPluginGuiInfo)
|
||||
@@ -56,7 +60,16 @@ end
|
||||
function StudioPluginGui:render()
|
||||
return e(Roact.Portal, {
|
||||
target = self.pluginGui,
|
||||
}, self.props[Roact.Children])
|
||||
}, {
|
||||
Background = Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
ZIndex = 0,
|
||||
BorderSizePixel = 0,
|
||||
}, self.props[Roact.Children])
|
||||
end),
|
||||
})
|
||||
end
|
||||
|
||||
function StudioPluginGui:didUpdate(lastProps)
|
||||
@@ -74,11 +87,14 @@ end
|
||||
local function StudioPluginGuiWrapper(props)
|
||||
return e(StudioPluginContext.Consumer, {
|
||||
render = function(plugin)
|
||||
return e(StudioPluginGui, Dictionary.merge(props, {
|
||||
plugin = plugin,
|
||||
}))
|
||||
return e(
|
||||
StudioPluginGui,
|
||||
Dictionary.merge(props, {
|
||||
plugin = plugin,
|
||||
})
|
||||
)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return StudioPluginGuiWrapper
|
||||
return StudioPluginGuiWrapper
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Dictionary = require(Plugin.Dictionary)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Dictionary = require(Plugin.Dictionary)
|
||||
|
||||
@@ -42,4 +43,4 @@ local function StudioToolbarWrapper(props)
|
||||
})
|
||||
end
|
||||
|
||||
return StudioToolbarWrapper
|
||||
return StudioToolbarWrapper
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local StudioToolbarContext = Roact.createContext(nil)
|
||||
|
||||
return StudioToolbarContext
|
||||
return StudioToolbarContext
|
||||
|
||||
@@ -2,9 +2,10 @@ local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Flipper = require(Rojo.Flipper)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
@@ -130,8 +131,10 @@ function TextButton:render()
|
||||
|
||||
zIndex = -2,
|
||||
}),
|
||||
|
||||
Children = Roact.createFragment(self.props[Roact.Children]),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return TextButton
|
||||
return TextButton
|
||||
|
||||
226
plugin/src/App/Components/Tooltip.lua
Normal file
226
plugin/src/App/Components/Tooltip.lua
Normal file
@@ -0,0 +1,226 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local DELAY = 0.75 -- How long to hover before a popup is shown (seconds)
|
||||
local TEXT_PADDING = Vector2.new(8 * 2, 6 * 2) -- Padding for the popup text containers
|
||||
local TAIL_SIZE = 16 -- Size of the triangle tail piece
|
||||
local X_OFFSET = 30 -- How far right (from left) the tail will be (assuming enough space)
|
||||
local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to help "connect" it
|
||||
|
||||
local TooltipContext = Roact.createContext({})
|
||||
|
||||
local function Popup(props)
|
||||
local textSize = TextService:GetTextSize(
|
||||
props.Text, 16, Enum.Font.GothamMedium, Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
||||
) + TEXT_PADDING + (Vector2.one * 2)
|
||||
|
||||
local trigger = props.Trigger:getValue()
|
||||
|
||||
local spaceBelow = props.parentSize.Y - (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
|
||||
|
||||
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||
local displayAbove = spaceBelow < textSize.Y and spaceAbove > spaceBelow
|
||||
|
||||
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
|
||||
local Y = 0
|
||||
|
||||
if displayAbove then
|
||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
||||
else
|
||||
Y = math.min(trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP, props.parentSize.Y - textSize.Y)
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e(BorderedContainer, {
|
||||
position = UDim2.fromOffset(X, Y),
|
||||
size = UDim2.fromOffset(textSize.X, textSize.Y),
|
||||
transparency = props.transparency,
|
||||
}, {
|
||||
Label = e("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.new(1, -TEXT_PADDING.X, 1, -TEXT_PADDING.Y),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Text = props.Text,
|
||||
TextSize = 16,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextWrapped = true,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
}),
|
||||
|
||||
Tail = e("ImageLabel", {
|
||||
ZIndex = 100,
|
||||
Position =
|
||||
if displayAbove then
|
||||
UDim2.new(
|
||||
0, math.clamp(props.Position.X - X, 6, textSize.X-6),
|
||||
1, -1
|
||||
)
|
||||
else
|
||||
UDim2.new(
|
||||
0, math.clamp(props.Position.X - X, 6, textSize.X-6),
|
||||
0, -TAIL_SIZE+1
|
||||
),
|
||||
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Rotation = if displayAbove then 180 else 0,
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxassetid://10983945016",
|
||||
ImageColor3 = theme.BorderedContainer.BackgroundColor,
|
||||
ImageTransparency = props.transparency,
|
||||
}, {
|
||||
Border = e("ImageLabel", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
Image = "rbxassetid://10983946430",
|
||||
ImageColor3 = theme.BorderedContainer.BorderColor,
|
||||
ImageTransparency = props.transparency,
|
||||
}),
|
||||
})
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local Provider = Roact.Component:extend("TooltipManager")
|
||||
|
||||
function Provider:init()
|
||||
self:setState({
|
||||
tips = {},
|
||||
addTip = function(id: string, data: { Text: string, Position: Vector2, Trigger: any })
|
||||
self:setState(function(state)
|
||||
state.tips[id] = data
|
||||
return state
|
||||
end)
|
||||
end,
|
||||
removeTip = function(id: string)
|
||||
self:setState(function(state)
|
||||
state.tips[id] = nil
|
||||
return state
|
||||
end)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function Provider:render()
|
||||
return Roact.createElement(TooltipContext.Provider, {
|
||||
value = self.state,
|
||||
}, self.props[Roact.Children])
|
||||
end
|
||||
|
||||
local Container = Roact.Component:extend("TooltipContainer")
|
||||
|
||||
function Container:init()
|
||||
self:setState({
|
||||
size = Vector2.new(200, 100),
|
||||
})
|
||||
end
|
||||
|
||||
function Container:render()
|
||||
return Roact.createElement(TooltipContext.Consumer, {
|
||||
render = function(context)
|
||||
local tips = context.tips
|
||||
local popups = {}
|
||||
|
||||
for key, value in tips do
|
||||
popups[key] = e(Popup, {
|
||||
Text = value.Text or "",
|
||||
Position = value.Position or Vector2.zero,
|
||||
Trigger = value.Trigger,
|
||||
|
||||
parentSize = self.state.size,
|
||||
})
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||
self:setState({
|
||||
size = rbx.AbsoluteSize,
|
||||
})
|
||||
end,
|
||||
ZIndex = 100,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
}, popups)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local Trigger = Roact.Component:extend("TooltipTrigger")
|
||||
|
||||
function Trigger:init()
|
||||
self.id = HttpService:GenerateGUID(false)
|
||||
self.ref = Roact.createRef()
|
||||
self.mousePos = Vector2.zero
|
||||
|
||||
self.destroy = function()
|
||||
self.props.context.removeTip(self.id)
|
||||
end
|
||||
end
|
||||
|
||||
function Trigger:willUnmount()
|
||||
if self.showDelayThread then
|
||||
task.cancel(self.showDelayThread)
|
||||
end
|
||||
if self.destroy then
|
||||
self.destroy()
|
||||
end
|
||||
end
|
||||
|
||||
function Trigger:render()
|
||||
return e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
ZIndex = self.props.zIndex or 100,
|
||||
[Roact.Ref] = self.ref,
|
||||
|
||||
[Roact.Event.MouseMoved] = function(_rbx, x, y)
|
||||
self.mousePos = Vector2.new(x, y)
|
||||
end,
|
||||
[Roact.Event.MouseEnter] = function()
|
||||
self.showDelayThread = task.delay(DELAY, function()
|
||||
self.props.context.addTip(self.id, {
|
||||
Text = self.props.text,
|
||||
Position = self.mousePos,
|
||||
Trigger = self.ref,
|
||||
})
|
||||
end)
|
||||
end,
|
||||
[Roact.Event.MouseLeave] = function()
|
||||
if self.showDelayThread then
|
||||
task.cancel(self.showDelayThread)
|
||||
end
|
||||
self.props.context.removeTip(self.id)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function TriggerConsumer(props)
|
||||
return Roact.createElement(TooltipContext.Consumer, {
|
||||
render = function(context)
|
||||
local innerProps = table.clone(props)
|
||||
innerProps.context = context
|
||||
|
||||
return e(Trigger, innerProps)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return {
|
||||
Provider = Provider,
|
||||
Container = Container,
|
||||
Trigger = TriggerConsumer,
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Flipper = require(Rojo.Flipper)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
@@ -142,4 +143,4 @@ function TouchRipple:render()
|
||||
})
|
||||
end
|
||||
|
||||
return TouchRipple
|
||||
return TouchRipple
|
||||
|
||||
156
plugin/src/App/Components/VirtualScroller.lua
Normal file
156
plugin/src/App/Components/VirtualScroller.lua
Normal file
@@ -0,0 +1,156 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local VirtualScroller = Roact.Component:extend("VirtualScroller")
|
||||
|
||||
function VirtualScroller:init()
|
||||
self.scrollFrameRef = Roact.createRef()
|
||||
self:setState({
|
||||
WindowSize = Vector2.new(),
|
||||
CanvasPosition = Vector2.new(),
|
||||
})
|
||||
|
||||
self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0)
|
||||
self.padding, self.setPadding = Roact.createBinding(0)
|
||||
|
||||
self:refresh()
|
||||
if self.props.updateEvent then
|
||||
self.connection = self.props.updateEvent:Connect(function()
|
||||
self:refresh()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function VirtualScroller:didMount()
|
||||
local rbx = self.scrollFrameRef:getValue()
|
||||
|
||||
local windowSizeSignal = rbx:GetPropertyChangedSignal("AbsoluteWindowSize")
|
||||
self.windowSizeChanged = windowSizeSignal:Connect(function()
|
||||
self:setState({ WindowSize = rbx.AbsoluteWindowSize })
|
||||
self:refresh()
|
||||
end)
|
||||
|
||||
local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition")
|
||||
self.canvasPositionChanged = canvasPositionSignal:Connect(function()
|
||||
if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then
|
||||
self:setState({ CanvasPosition = rbx.CanvasPosition })
|
||||
self:refresh()
|
||||
end
|
||||
end)
|
||||
|
||||
self:refresh()
|
||||
end
|
||||
|
||||
function VirtualScroller:willUnmount()
|
||||
self.windowSizeChanged:Disconnect()
|
||||
self.canvasPositionChanged:Disconnect()
|
||||
if self.connection then
|
||||
self.connection:Disconnect()
|
||||
self.connection = nil
|
||||
end
|
||||
end
|
||||
|
||||
function VirtualScroller:refresh()
|
||||
local props = self.props
|
||||
local state = self.state
|
||||
|
||||
local count = props.count
|
||||
local windowSize, canvasPosition = state.WindowSize.Y, state.CanvasPosition.Y
|
||||
local bottom = canvasPosition + windowSize
|
||||
|
||||
local minIndex, maxIndex = 1, count
|
||||
local padding, canvasSize = 0, 0
|
||||
|
||||
local pos = 0
|
||||
for i = 1, count do
|
||||
local height = props.getHeightBinding(i):getValue()
|
||||
canvasSize += height
|
||||
|
||||
if pos > bottom then
|
||||
-- Below window
|
||||
if maxIndex > i then
|
||||
maxIndex = i
|
||||
end
|
||||
end
|
||||
|
||||
pos += height
|
||||
|
||||
if pos < canvasPosition then
|
||||
-- Above window
|
||||
minIndex = i
|
||||
padding = pos - height
|
||||
end
|
||||
end
|
||||
|
||||
self.setPadding(padding)
|
||||
self.setTotalCanvas(canvasSize)
|
||||
self:setState({
|
||||
Start = minIndex,
|
||||
End = maxIndex,
|
||||
})
|
||||
end
|
||||
|
||||
function VirtualScroller:render()
|
||||
local props, state = self.props, self.state
|
||||
|
||||
local items = {}
|
||||
for i = state.Start, state.End do
|
||||
items["Item" .. i] = e("Frame", {
|
||||
LayoutOrder = i,
|
||||
Size = props.getHeightBinding(i):map(function(height)
|
||||
return UDim2.new(1, 0, 0, height)
|
||||
end),
|
||||
BackgroundTransparency = 1,
|
||||
}, props.render(i))
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e("ScrollingFrame", {
|
||||
Size = props.size,
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorPoint,
|
||||
BackgroundTransparency = props.backgroundTransparency or 1,
|
||||
BackgroundColor3 = props.backgroundColor3,
|
||||
BorderColor3 = props.borderColor3,
|
||||
CanvasSize = self.totalCanvas:map(function(s)
|
||||
return UDim2.fromOffset(0, s)
|
||||
end),
|
||||
ScrollBarThickness = 9,
|
||||
ScrollBarImageColor3 = theme.ScrollBarColor,
|
||||
ScrollBarImageTransparency = props.transparency:map(function(value)
|
||||
return bindingUtil.blendAlpha({ 0.65, value })
|
||||
end),
|
||||
TopImage = Assets.Images.ScrollBar.Top,
|
||||
MidImage = Assets.Images.ScrollBar.Middle,
|
||||
BottomImage = Assets.Images.ScrollBar.Bottom,
|
||||
|
||||
ElasticBehavior = Enum.ElasticBehavior.Always,
|
||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||
[Roact.Ref] = self.scrollFrameRef,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
Padding = UDim.new(0, 0),
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
}),
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = self.padding:map(function(p)
|
||||
return UDim.new(0, p)
|
||||
end),
|
||||
}),
|
||||
Content = Roact.createFragment(items),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return VirtualScroller
|
||||
@@ -3,15 +3,15 @@ local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Flipper = require(Rojo.Flipper)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local bindingUtil = require(script.Parent.bindingUtil)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local playSound = require(Plugin.playSound)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
|
||||
@@ -53,7 +53,7 @@ function Notification:didMount()
|
||||
})
|
||||
)
|
||||
|
||||
playSound(Assets.Sounds.Notification)
|
||||
self.props.soundPlayer:play(Assets.Sounds.Notification)
|
||||
|
||||
self.timeout = task.spawn(function()
|
||||
local clock = os.clock()
|
||||
@@ -182,6 +182,7 @@ function Notifications:render()
|
||||
|
||||
for index, notif in ipairs(self.props.notifications) do
|
||||
notifs[notif] = e(Notification, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
text = notif.text,
|
||||
timestamp = notif.timestamp,
|
||||
timeout = notif.timeout,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Flipper = require(Rojo.Flipper)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local Dictionary = require(Plugin.Dictionary)
|
||||
|
||||
@@ -67,4 +68,4 @@ function Page:didUpdate(lastProps)
|
||||
end
|
||||
end
|
||||
|
||||
return Page
|
||||
return Page
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
--[[
|
||||
Persistent plugin settings that can be accessed via Roact context.
|
||||
]]
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
|
||||
local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
showNotifications = true,
|
||||
}
|
||||
|
||||
local Settings = {}
|
||||
Settings.__index = Settings
|
||||
|
||||
function Settings.fromPlugin(plugin)
|
||||
local values = {}
|
||||
|
||||
for name, defaultValue in pairs(defaultSettings) do
|
||||
local savedValue = plugin:GetSetting("Rojo_" .. name)
|
||||
|
||||
if savedValue == nil then
|
||||
plugin:SetSetting("Rojo_" .. name, defaultValue)
|
||||
values[name] = defaultValue
|
||||
else
|
||||
values[name] = savedValue
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
__values = values,
|
||||
__plugin = plugin,
|
||||
__updateListeners = {},
|
||||
}, Settings)
|
||||
end
|
||||
|
||||
function Settings:get(name)
|
||||
if defaultSettings[name] == nil then
|
||||
error("Invalid setings name " .. tostring(name), 2)
|
||||
end
|
||||
|
||||
return self.__values[name]
|
||||
end
|
||||
|
||||
function Settings:set(name, value)
|
||||
self.__plugin:SetSetting("Rojo_" .. name, value)
|
||||
self.__values[name] = value
|
||||
|
||||
for callback in pairs(self.__updateListeners) do
|
||||
callback(name, value)
|
||||
end
|
||||
end
|
||||
|
||||
function Settings:onUpdate(newCallback)
|
||||
local newListeners = {}
|
||||
for callback in pairs(self.__updateListeners) do
|
||||
newListeners[callback] = true
|
||||
end
|
||||
|
||||
newListeners[newCallback] = true
|
||||
self.__updateListeners = newListeners
|
||||
|
||||
return function()
|
||||
local newListeners = {}
|
||||
for callback in pairs(self.__updateListeners) do
|
||||
if callback ~= newCallback then
|
||||
newListeners[callback] = true
|
||||
end
|
||||
end
|
||||
|
||||
self.__updateListeners = newListeners
|
||||
end
|
||||
end
|
||||
|
||||
local Context = Roact.createContext(nil)
|
||||
|
||||
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||
|
||||
function StudioProvider:init()
|
||||
self.settings = Settings.fromPlugin(self.props.plugin)
|
||||
end
|
||||
|
||||
function StudioProvider:render()
|
||||
return Roact.createElement(Context.Provider, {
|
||||
value = self.settings,
|
||||
}, self.props[Roact.Children])
|
||||
end
|
||||
|
||||
local InternalConsumer = Roact.Component:extend("InternalConsumer")
|
||||
|
||||
function InternalConsumer:render()
|
||||
return self.props.render(self.props.settings)
|
||||
end
|
||||
|
||||
function InternalConsumer:didMount()
|
||||
self.disconnect = self.props.settings:onUpdate(function()
|
||||
-- Trigger a dummy state update to update the settings consumer.
|
||||
self:setState({})
|
||||
end)
|
||||
end
|
||||
|
||||
function InternalConsumer:willUnmount()
|
||||
self.disconnect()
|
||||
end
|
||||
|
||||
local function with(callback)
|
||||
return Roact.createElement(Context.Consumer, {
|
||||
render = function(settings)
|
||||
return Roact.createElement(InternalConsumer, {
|
||||
settings = settings,
|
||||
render = callback,
|
||||
})
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return {
|
||||
StudioProvider = StudioProvider,
|
||||
with = with,
|
||||
}
|
||||
156
plugin/src/App/StatusPages/Confirming.lua
Normal file
156
plugin/src/App/StatusPages/Confirming.lua
Normal file
@@ -0,0 +1,156 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local Header = require(Plugin.App.Components.Header)
|
||||
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local ConfirmingPage = Roact.Component:extend("ConfirmingPage")
|
||||
|
||||
function ConfirmingPage:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
end
|
||||
|
||||
function ConfirmingPage:render()
|
||||
return Theme.with(function(theme)
|
||||
local pageContent = Roact.createFragment({
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
Title = e("TextLabel", {
|
||||
Text = string.format(
|
||||
"Sync changes for project '%s':",
|
||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||
),
|
||||
LayoutOrder = 2,
|
||||
Font = Enum.Font.Gotham,
|
||||
LineHeight = 1.2,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
PatchVisualizer = e(PatchVisualizer, {
|
||||
size = UDim2.new(1, 0, 1, -150),
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
|
||||
columnVisibility = {true, true, true},
|
||||
patch = self.props.confirmData.patch,
|
||||
instanceMap = self.props.confirmData.instanceMap,
|
||||
}),
|
||||
|
||||
Buttons = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 34),
|
||||
LayoutOrder = 4,
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Abort = e(TextButton, {
|
||||
text = "Abort",
|
||||
style = "Bordered",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
onClick = self.props.onAbort,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Stop the connection process"
|
||||
}),
|
||||
}),
|
||||
|
||||
Reject = if Settings:get("twoWaySync")
|
||||
then e(TextButton, {
|
||||
text = "Reject",
|
||||
style = "Bordered",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
onClick = self.props.onReject,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Push Studio changes to the Rojo server"
|
||||
}),
|
||||
})
|
||||
else nil,
|
||||
|
||||
Accept = e(TextButton, {
|
||||
text = "Accept",
|
||||
style = "Solid",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
onClick = self.props.onAccept,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Pull Rojo server changes to Studio"
|
||||
}),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
})
|
||||
|
||||
if self.props.createPopup then
|
||||
return e(StudioPluginGui, {
|
||||
id = "Rojo_DiffSync",
|
||||
title = string.format(
|
||||
"Confirm sync for project '%s':",
|
||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||
),
|
||||
active = true,
|
||||
|
||||
initDockState = Enum.InitialDockState.Float,
|
||||
initEnabled = true,
|
||||
overridePreviousState = true,
|
||||
floatingSize = Vector2.new(500, 350),
|
||||
minimumSize = Vector2.new(400, 250),
|
||||
|
||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
|
||||
onClose = self.props.onAbort,
|
||||
}, {
|
||||
Tooltips = e(Tooltip.Container, nil),
|
||||
Content = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, pageContent),
|
||||
})
|
||||
end
|
||||
|
||||
return pageContent
|
||||
end)
|
||||
end
|
||||
|
||||
return ConfirmingPage
|
||||
@@ -1,17 +1,87 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local PatchSet = require(Plugin.PatchSet)
|
||||
|
||||
local Header = require(Plugin.App.Components.Header)
|
||||
local IconButton = require(Plugin.App.Components.IconButton)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local AGE_UNITS = { {31556909, "year"}, {2629743, "month"}, {604800, "week"}, {86400, "day"}, {3600, "hour"}, {60, "minute"}, }
|
||||
function timeSinceText(elapsed: number): string
|
||||
if elapsed < 3 then
|
||||
return "just now"
|
||||
end
|
||||
|
||||
local ageText = string.format("%d seconds ago", elapsed)
|
||||
|
||||
for _,UnitData in ipairs(AGE_UNITS) do
|
||||
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
|
||||
if elapsed > UnitSeconds then
|
||||
local c = math.floor(elapsed/UnitSeconds)
|
||||
ageText = string.format("%d %s%s ago", c, UnitName, c>1 and "s" or "")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return ageText
|
||||
end
|
||||
|
||||
local function ChangesDrawer(props)
|
||||
if props.rendered == false then
|
||||
return nil
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e(BorderedContainer, {
|
||||
transparency = props.transparency,
|
||||
size = props.height:map(function(y)
|
||||
return UDim2.new(1, 0, y, -180 * y)
|
||||
end),
|
||||
position = UDim2.new(0, 0, 1, 0),
|
||||
anchorPoint = Vector2.new(0, 1),
|
||||
layoutOrder = props.layoutOrder,
|
||||
}, {
|
||||
Close = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Close,
|
||||
iconSize = 24,
|
||||
color = theme.ConnectionDetails.DisconnectColor,
|
||||
transparency = props.transparency,
|
||||
|
||||
position = UDim2.new(1, 0, 0, 0),
|
||||
anchorPoint = Vector2.new(1, 0),
|
||||
|
||||
onClick = props.onClose,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Close the patch visualizer"
|
||||
}),
|
||||
}),
|
||||
|
||||
PatchVisualizer = e(PatchVisualizer, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
transparency = props.transparency,
|
||||
layoutOrder = 3,
|
||||
|
||||
columnVisibility = {true, false, true},
|
||||
patch = props.patchInfo:getValue().patch,
|
||||
instanceMap = props.serveSession.__instanceMap,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local function ConnectionDetails(props)
|
||||
return Theme.with(function(theme)
|
||||
return e(BorderedContainer, {
|
||||
@@ -69,6 +139,10 @@ local function ConnectionDetails(props)
|
||||
anchorPoint = Vector2.new(1, 0.5),
|
||||
|
||||
onClick = props.onDisconnect,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Disconnect from the Rojo sync server"
|
||||
}),
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
@@ -81,36 +155,116 @@ end
|
||||
|
||||
local ConnectedPage = Roact.Component:extend("ConnectedPage")
|
||||
|
||||
function ConnectedPage:render()
|
||||
return Roact.createFragment({
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
function ConnectedPage:init()
|
||||
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
|
||||
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
|
||||
|
||||
ConnectionDetails = e(ConnectionDetails, {
|
||||
projectName = self.state.projectName,
|
||||
address = self.state.address,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
self.changeDrawerMotor:onStep(function(value)
|
||||
local renderChanges = value > 0.05
|
||||
|
||||
onDisconnect = self.props.onDisconnect,
|
||||
}),
|
||||
self:setState(function(state)
|
||||
if state.renderChanges == renderChanges then
|
||||
return nil
|
||||
end
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
return {
|
||||
renderChanges = renderChanges,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
self:setState({
|
||||
renderChanges = false,
|
||||
})
|
||||
end
|
||||
|
||||
function ConnectedPage:render()
|
||||
return Theme.with(function(theme)
|
||||
return Roact.createFragment({
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
ConnectionDetails = e(ConnectionDetails, {
|
||||
projectName = self.state.projectName,
|
||||
address = self.state.address,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
|
||||
onDisconnect = self.props.onDisconnect,
|
||||
}),
|
||||
|
||||
ChangeInfo = e("TextButton", {
|
||||
Text = self.props.patchInfo:map(function(info)
|
||||
local changes = PatchSet.countChanges(info.patch)
|
||||
return string.format(
|
||||
"<i>Synced %d change%s %s</i>",
|
||||
changes,
|
||||
changes == 1 and "" or "s",
|
||||
timeSinceText(os.time() - info.timestamp)
|
||||
)
|
||||
end),
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 14,
|
||||
TextWrapped = true,
|
||||
RichText = true,
|
||||
TextColor3 = theme.Header.VersionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 28),
|
||||
|
||||
LayoutOrder = 3,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
if self.state.renderChanges then
|
||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||
frequency = 4,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
else
|
||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
|
||||
frequency = 3,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
end
|
||||
end,
|
||||
}),
|
||||
|
||||
ChangesDrawer = e(ChangesDrawer, {
|
||||
rendered = self.state.renderChanges,
|
||||
transparency = self.props.transparency,
|
||||
patchInfo = self.props.patchInfo,
|
||||
serveSession = self.props.serveSession,
|
||||
height = self.changeDrawerHeight,
|
||||
layoutOrder = 4,
|
||||
|
||||
onClose = function()
|
||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||
frequency = 4,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
end,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
function ConnectedPage.getDerivedStateFromProps(props)
|
||||
-- If projectName or address ever get removed from props, make sure we still have
|
||||
-- the properties! The component still needs to have its data for it to be properly
|
||||
@@ -122,4 +276,4 @@ function ConnectedPage.getDerivedStateFromProps(props)
|
||||
}
|
||||
end
|
||||
|
||||
return ConnectedPage
|
||||
return ConnectedPage
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Spinner = require(Plugin.App.Components.Spinner)
|
||||
|
||||
@@ -17,4 +18,4 @@ function ConnectingPage:render()
|
||||
})
|
||||
end
|
||||
|
||||
return ConnectingPage
|
||||
return ConnectingPage
|
||||
|
||||
@@ -2,14 +2,16 @@ local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -56,8 +58,16 @@ function Error:render()
|
||||
end,
|
||||
}, {
|
||||
ErrorMessage = Theme.with(function(theme)
|
||||
return e("TextLabel", {
|
||||
return e("TextBox", {
|
||||
[Roact.Event.InputBegan] = function(rbx, input)
|
||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
|
||||
rbx.SelectionStart = 0
|
||||
rbx.CursorPosition = #rbx.Text+1
|
||||
end,
|
||||
|
||||
|
||||
Text = self.props.errorMessage,
|
||||
TextEditable = false,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 16,
|
||||
TextColor3 = theme.ErrorColor,
|
||||
@@ -65,10 +75,9 @@ function Error:render()
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextTransparency = self.props.transparency,
|
||||
TextWrapped = true,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
ClearTextOnFocus = false,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
})
|
||||
end),
|
||||
|
||||
@@ -115,6 +124,10 @@ function ErrorPage:render()
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
onClick = self.props.onClose,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Dismiss message"
|
||||
}),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
@@ -150,4 +163,4 @@ function ErrorPage.getDerivedStateFromProps(props)
|
||||
}
|
||||
end
|
||||
|
||||
return ErrorPage
|
||||
return ErrorPage
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Config = require(Plugin.Config)
|
||||
|
||||
@@ -9,6 +10,7 @@ local Theme = require(Plugin.App.Theme)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local Header = require(Plugin.App.Components.Header)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
|
||||
local PORT_WIDTH = 74
|
||||
local DIVIDER_WIDTH = 1
|
||||
@@ -108,6 +110,7 @@ function NotConnectedPage:render()
|
||||
Size = UDim2.new(1, 0, 0, 34),
|
||||
LayoutOrder = 3,
|
||||
BackgroundTransparency = 1,
|
||||
ZIndex = 2,
|
||||
}, {
|
||||
Settings = e(TextButton, {
|
||||
text = "Settings",
|
||||
@@ -115,6 +118,10 @@ function NotConnectedPage:render()
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
onClick = self.props.onNavigateSettings,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "View and modify plugin settings"
|
||||
}),
|
||||
}),
|
||||
|
||||
Connect = e(TextButton, {
|
||||
@@ -123,6 +130,10 @@ function NotConnectedPage:render()
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
onClick = self.props.onConnect,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Connect to a Rojo sync server"
|
||||
}),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
@@ -148,4 +159,4 @@ function NotConnectedPage:render()
|
||||
})
|
||||
end
|
||||
|
||||
return NotConnectedPage
|
||||
return NotConnectedPage
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local PluginSettings = require(Plugin.App.PluginSettings)
|
||||
|
||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
||||
local IconButton = require(Plugin.App.Components.IconButton)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local DIVIDER_FADE_SIZE = 0.1
|
||||
|
||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
||||
|
||||
local lineCount = textBounds.Y / textSize
|
||||
local lineHeightAbsolute = textSize * lineHeight
|
||||
|
||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||
end
|
||||
|
||||
local function Navbar(props)
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings.Navbar
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 46),
|
||||
LayoutOrder = props.layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Back = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Back,
|
||||
iconSize = 24,
|
||||
color = theme.BackButtonColor,
|
||||
transparency = props.transparency,
|
||||
|
||||
position = UDim2.new(0, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(0, 0.5),
|
||||
|
||||
onClick = props.onBack,
|
||||
}),
|
||||
|
||||
Text = e("TextLabel", {
|
||||
Text = "Settings",
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 18,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local Setting = Roact.Component:extend("Setting")
|
||||
|
||||
function Setting:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
end
|
||||
|
||||
function Setting:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings
|
||||
|
||||
return PluginSettings.with(function(settings)
|
||||
return e("Frame", {
|
||||
Size = self.contentSize:map(function(value)
|
||||
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
|
||||
end),
|
||||
LayoutOrder = self.props.layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
self.setContainerSize(object.AbsoluteSize)
|
||||
end,
|
||||
}, {
|
||||
Checkbox = e(Checkbox, {
|
||||
active = settings:get(self.props.id),
|
||||
transparency = self.props.transparency,
|
||||
position = UDim2.new(1, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(1, 0.5),
|
||||
onClick = function()
|
||||
local currentValue = settings:get(self.props.id)
|
||||
settings:set(self.props.id, not currentValue)
|
||||
end,
|
||||
}),
|
||||
|
||||
Text = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Name = e("TextLabel", {
|
||||
Text = self.props.name,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 17,
|
||||
TextColor3 = theme.Setting.NameColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 17),
|
||||
|
||||
LayoutOrder = 1,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Description = e("TextLabel", {
|
||||
Text = self.props.description,
|
||||
Font = Enum.Font.Gotham,
|
||||
LineHeight = 1.2,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
TextWrapped = true,
|
||||
|
||||
Size = self.containerSize:map(function(value)
|
||||
local textBounds = getTextBounds(
|
||||
self.props.description, 14, Enum.Font.Gotham, 1.2,
|
||||
Vector2.new(value.X - 50, math.huge)
|
||||
)
|
||||
return UDim2.new(1, -50, 0, textBounds.Y)
|
||||
end),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 6),
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 20),
|
||||
PaddingBottom = UDim.new(0, 20),
|
||||
}),
|
||||
}),
|
||||
|
||||
Divider = e("Frame", {
|
||||
BackgroundColor3 = theme.DividerColor,
|
||||
BackgroundTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
Gradient = e("UIGradient", {
|
||||
Transparency = NumberSequence.new({
|
||||
NumberSequenceKeypoint.new(0, 1),
|
||||
NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0),
|
||||
NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0),
|
||||
NumberSequenceKeypoint.new(1, 1),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local SettingsPage = Roact.Component:extend("SettingsPage")
|
||||
|
||||
function SettingsPage:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
end
|
||||
|
||||
function SettingsPage:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings
|
||||
|
||||
return e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize,
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
Navbar = e(Navbar, {
|
||||
onBack = self.props.onBack,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 0,
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "Attempt to open scripts in an external editor",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
ShowNotifications = e(Setting, {
|
||||
id = "showNotifications",
|
||||
name = "Show Notifications",
|
||||
description = "Popup notifications in viewport",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
TwoWaySync = e(Setting, {
|
||||
id = "twoWaySync",
|
||||
name = "Two-Way Sync",
|
||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return SettingsPage
|
||||
180
plugin/src/App/StatusPages/Settings/Setting.lua
Normal file
180
plugin/src/App/StatusPages/Settings/Setting.lua
Normal file
@@ -0,0 +1,180 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
||||
local Dropdown = require(Plugin.App.Components.Dropdown)
|
||||
local IconButton = require(Plugin.App.Components.IconButton)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local DIVIDER_FADE_SIZE = 0.1
|
||||
|
||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
||||
|
||||
local lineCount = textBounds.Y / textSize
|
||||
local lineHeightAbsolute = textSize * lineHeight
|
||||
|
||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||
end
|
||||
|
||||
local Setting = Roact.Component:extend("Setting")
|
||||
|
||||
function Setting:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
self:setState({
|
||||
setting = Settings:get(self.props.id),
|
||||
})
|
||||
|
||||
self.changedCleanup = Settings:onChanged(self.props.id, function(value)
|
||||
self:setState({
|
||||
setting = value,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
function Setting:willUnmount()
|
||||
self.changedCleanup()
|
||||
end
|
||||
|
||||
function Setting:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings
|
||||
|
||||
return e("Frame", {
|
||||
Size = self.contentSize:map(function(value)
|
||||
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
|
||||
end),
|
||||
LayoutOrder = self.props.layoutOrder,
|
||||
ZIndex = -self.props.layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
self.setContainerSize(object.AbsoluteSize)
|
||||
end,
|
||||
}, {
|
||||
Input = if self.props.options ~= nil then
|
||||
e(Dropdown, {
|
||||
options = self.props.options,
|
||||
active = self.state.setting,
|
||||
transparency = self.props.transparency,
|
||||
position = UDim2.new(1, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(1, 0.5),
|
||||
onClick = function(option)
|
||||
Settings:set(self.props.id, option)
|
||||
end,
|
||||
})
|
||||
else
|
||||
e(Checkbox, {
|
||||
active = self.state.setting,
|
||||
transparency = self.props.transparency,
|
||||
position = UDim2.new(1, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(1, 0.5),
|
||||
onClick = function()
|
||||
local currentValue = Settings:get(self.props.id)
|
||||
Settings:set(self.props.id, not currentValue)
|
||||
end,
|
||||
}),
|
||||
|
||||
Reset = if self.props.onReset then e(IconButton, {
|
||||
icon = Assets.Images.Icons.Reset,
|
||||
iconSize = 24,
|
||||
color = theme.BackButtonColor,
|
||||
transparency = self.props.transparency,
|
||||
visible = self.props.showReset,
|
||||
|
||||
position = UDim2.new(1, -32 - (self.props.options ~= nil and 120 or 40), 0.5, 0),
|
||||
anchorPoint = Vector2.new(0, 0.5),
|
||||
|
||||
onClick = self.props.onReset,
|
||||
}) else nil,
|
||||
|
||||
Text = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Name = e("TextLabel", {
|
||||
Text = self.props.name,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 17,
|
||||
TextColor3 = theme.Setting.NameColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 17),
|
||||
|
||||
LayoutOrder = 1,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Description = e("TextLabel", {
|
||||
Text = self.props.description,
|
||||
Font = Enum.Font.Gotham,
|
||||
LineHeight = 1.2,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
TextWrapped = true,
|
||||
|
||||
Size = self.containerSize:map(function(value)
|
||||
local offset = (self.props.onReset and 34 or 0) + (self.props.options ~= nil and 120 or 40)
|
||||
local textBounds = getTextBounds(
|
||||
self.props.description, 14, Enum.Font.Gotham, 1.2,
|
||||
Vector2.new(value.X - offset, math.huge)
|
||||
)
|
||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||
end),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 6),
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 20),
|
||||
PaddingBottom = UDim.new(0, 20),
|
||||
}),
|
||||
}),
|
||||
|
||||
Divider = e("Frame", {
|
||||
BackgroundColor3 = theme.DividerColor,
|
||||
BackgroundTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
Gradient = e("UIGradient", {
|
||||
Transparency = NumberSequence.new({
|
||||
NumberSequenceKeypoint.new(0, 1),
|
||||
NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0),
|
||||
NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0),
|
||||
NumberSequenceKeypoint.new(1, 1),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return Setting
|
||||
163
plugin/src/App/StatusPages/Settings/init.lua
Normal file
163
plugin/src/App/StatusPages/Settings/init.lua
Normal file
@@ -0,0 +1,163 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local IconButton = require(Plugin.App.Components.IconButton)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local Setting = require(script.Setting)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local function invertTbl(tbl)
|
||||
local new = {}
|
||||
for key, value in tbl do
|
||||
new[value] = key
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
local invertedLevels = invertTbl(Log.Level)
|
||||
|
||||
local function Navbar(props)
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings.Navbar
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 46),
|
||||
LayoutOrder = props.layoutOrder,
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Back = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Back,
|
||||
iconSize = 24,
|
||||
color = theme.BackButtonColor,
|
||||
transparency = props.transparency,
|
||||
|
||||
position = UDim2.new(0, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(0, 0.5),
|
||||
|
||||
onClick = props.onBack,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Back"
|
||||
}),
|
||||
}),
|
||||
|
||||
Text = e("TextLabel", {
|
||||
Text = "Settings",
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 18,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local SettingsPage = Roact.Component:extend("SettingsPage")
|
||||
|
||||
function SettingsPage:init()
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
end
|
||||
|
||||
function SettingsPage:render()
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings
|
||||
|
||||
return e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize,
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
Navbar = e(Navbar, {
|
||||
onBack = self.props.onBack,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 0,
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "Attempt to open scripts in an external editor",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
ShowNotifications = e(Setting, {
|
||||
id = "showNotifications",
|
||||
name = "Show Notifications",
|
||||
description = "Popup notifications in viewport",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
PlaySounds = e(Setting, {
|
||||
id = "playSounds",
|
||||
name = "Play Sounds",
|
||||
description = "Toggle sound effects",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
}),
|
||||
|
||||
TwoWaySync = e(Setting, {
|
||||
id = "twoWaySync",
|
||||
name = "Two-Way Sync",
|
||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 4,
|
||||
}),
|
||||
|
||||
LogLevel = e(Setting, {
|
||||
id = "logLevel",
|
||||
name = "Log Level",
|
||||
description = "Plugin output verbosity level",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 5,
|
||||
|
||||
options = invertedLevels,
|
||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||
return value ~= "Info"
|
||||
end),
|
||||
onReset = function()
|
||||
Settings:set("logLevel", "Info")
|
||||
end,
|
||||
}),
|
||||
|
||||
TypecheckingEnabled = e(Setting, {
|
||||
id = "typecheckingEnabled",
|
||||
name = "Typechecking",
|
||||
description = "Toggle typechecking on the API surface",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 6,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return SettingsPage
|
||||
@@ -2,6 +2,7 @@ return {
|
||||
NotConnected = require(script.NotConnected),
|
||||
Settings = require(script.Settings),
|
||||
Connecting = require(script.Connecting),
|
||||
Confirming = require(script.Confirming),
|
||||
Connected = require(script.Connected),
|
||||
Error = require(script.Error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ local function getStudio()
|
||||
end
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Log = require(Rojo.Log)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local strict = require(script.Parent.Parent.strict)
|
||||
|
||||
@@ -71,6 +72,17 @@ local lightTheme = strict("LightTheme", {
|
||||
BorderColor = hexColor(0xAFAFAF),
|
||||
},
|
||||
},
|
||||
Dropdown = {
|
||||
TextColor = hexColor(0x00000),
|
||||
BorderColor = hexColor(0xAFAFAF),
|
||||
BackgroundColor = hexColor(0xEEEEEE),
|
||||
Open = {
|
||||
IconColor = BRAND_COLOR,
|
||||
},
|
||||
Closed = {
|
||||
IconColor = hexColor(0xEEEEEE),
|
||||
},
|
||||
},
|
||||
AddressEntry = {
|
||||
TextColor = hexColor(0x000000),
|
||||
PlaceholderColor = hexColor(0x8C8C8C)
|
||||
@@ -83,6 +95,12 @@ local lightTheme = strict("LightTheme", {
|
||||
ForegroundColor = BRAND_COLOR,
|
||||
BackgroundColor = hexColor(0xEEEEEE),
|
||||
},
|
||||
Diff = {
|
||||
Add = hexColor(0xbaffbd),
|
||||
Remove = hexColor(0xffbdba),
|
||||
Edit = hexColor(0xbacdff),
|
||||
Row = hexColor(0x000000),
|
||||
},
|
||||
ConnectionDetails = {
|
||||
ProjectNameColor = hexColor(0x00000),
|
||||
AddressColor = hexColor(0x00000),
|
||||
@@ -149,6 +167,17 @@ local darkTheme = strict("DarkTheme", {
|
||||
BorderColor = hexColor(0x5A5A5A),
|
||||
},
|
||||
},
|
||||
Dropdown = {
|
||||
TextColor = hexColor(0xFFFFFF),
|
||||
BorderColor = hexColor(0x5A5A5A),
|
||||
BackgroundColor = hexColor(0x2B2B2B),
|
||||
Open = {
|
||||
IconColor = BRAND_COLOR,
|
||||
},
|
||||
Closed = {
|
||||
IconColor = hexColor(0x484848),
|
||||
},
|
||||
},
|
||||
AddressEntry = {
|
||||
TextColor = hexColor(0xFFFFFF),
|
||||
PlaceholderColor = hexColor(0x8B8B8B)
|
||||
@@ -161,6 +190,12 @@ local darkTheme = strict("DarkTheme", {
|
||||
ForegroundColor = BRAND_COLOR,
|
||||
BackgroundColor = hexColor(0x2B2B2B),
|
||||
},
|
||||
Diff = {
|
||||
Add = hexColor(0x273732),
|
||||
Remove = hexColor(0x3F2D32),
|
||||
Edit = hexColor(0x193345),
|
||||
Row = hexColor(0xFFFFFF),
|
||||
},
|
||||
ConnectionDetails = {
|
||||
ProjectNameColor = hexColor(0xFFFFFF),
|
||||
AddressColor = hexColor(0xFFFFFF),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Log = require(Rojo.Log)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local LERP_DATA_TYPES = {
|
||||
Color3 = true,
|
||||
@@ -55,4 +56,4 @@ return {
|
||||
mapLerp = mapLerp,
|
||||
deriveProperty = deriveProperty,
|
||||
blendAlpha = blendAlpha,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
local Players = game:GetService("Players")
|
||||
local ServerStorage = game:GetService("ServerStorage")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Rojo.Roact)
|
||||
local Log = require(Rojo.Log)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Version = require(Plugin.Version)
|
||||
local Config = require(Plugin.Config)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local strict = require(Plugin.strict)
|
||||
local Dictionary = require(Plugin.Dictionary)
|
||||
local ServeSession = require(Plugin.ServeSession)
|
||||
local ApiContext = require(Plugin.ApiContext)
|
||||
local PatchSet = require(Plugin.PatchSet)
|
||||
local preloadAssets = require(Plugin.preloadAssets)
|
||||
local soundPlayer = require(Plugin.soundPlayer)
|
||||
local Theme = require(script.Theme)
|
||||
local PluginSettings = require(script.PluginSettings)
|
||||
|
||||
local Page = require(script.Page)
|
||||
local Notifications = require(script.Notifications)
|
||||
local Tooltip = require(script.Components.Tooltip)
|
||||
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
||||
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
||||
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
|
||||
@@ -28,6 +35,7 @@ local AppStatus = strict("AppStatus", {
|
||||
NotConnected = "NotConnected",
|
||||
Settings = "Settings",
|
||||
Connecting = "Connecting",
|
||||
Confirming = "Confirming",
|
||||
Connected = "Connected",
|
||||
Error = "Error",
|
||||
})
|
||||
@@ -39,19 +47,28 @@ local App = Roact.Component:extend("App")
|
||||
function App:init()
|
||||
preloadAssets()
|
||||
|
||||
self.host, self.setHost = Roact.createBinding("")
|
||||
self.port, self.setPort = Roact.createBinding("")
|
||||
local priorHost, priorPort = self:getPriorEndpoint()
|
||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
||||
|
||||
self.patchInfo, self.setPatchInfo = Roact.createBinding({
|
||||
patch = PatchSet.newEmpty(),
|
||||
timestamp = os.time(),
|
||||
})
|
||||
self.confirmationBindable = Instance.new("BindableEvent")
|
||||
self.confirmationEvent = self.confirmationBindable.Event
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
guiEnabled = false,
|
||||
confirmData = {},
|
||||
notifications = {},
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
end
|
||||
|
||||
function App:addNotification(text: string, timeout: number?)
|
||||
if not self.props.settings:get("showNotifications") then
|
||||
if not Settings:get("showNotifications") then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -76,6 +93,45 @@ function App:closeNotification(index: number)
|
||||
})
|
||||
end
|
||||
|
||||
function App:getPriorEndpoint()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then return end
|
||||
|
||||
local place = priorEndpoints[tostring(game.PlaceId)]
|
||||
if not place then return end
|
||||
|
||||
return place.host, place.port
|
||||
end
|
||||
|
||||
function App:setPriorEndpoint(host: string, port: string)
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
priorEndpoints = {}
|
||||
end
|
||||
|
||||
-- Clear any stale saves to avoid disc bloat
|
||||
for placeId, endpoint in priorEndpoints do
|
||||
if os.time() - endpoint.timestamp > 12_960_000 then
|
||||
priorEndpoints[placeId] = nil
|
||||
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
||||
end
|
||||
end
|
||||
|
||||
if host == Config.defaultHost and port == Config.defaultPort then
|
||||
-- Don't save default
|
||||
priorEndpoints[tostring(game.PlaceId)] = nil
|
||||
else
|
||||
priorEndpoints[tostring(game.PlaceId)] = {
|
||||
host = host ~= Config.defaultHost and host or nil,
|
||||
port = port ~= Config.defaultPort and port or nil,
|
||||
timestamp = os.time(),
|
||||
}
|
||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||
end
|
||||
|
||||
Settings:set("priorEndpoints", priorEndpoints)
|
||||
end
|
||||
|
||||
function App:getHostAndPort()
|
||||
local host = self.host:getValue()
|
||||
local port = self.port:getValue()
|
||||
@@ -86,15 +142,75 @@ function App:getHostAndPort()
|
||||
return host, port
|
||||
end
|
||||
|
||||
function App:claimSyncLock()
|
||||
if #Players:GetPlayers() == 0 then
|
||||
Log.trace("Skipping sync lock because this isn't in Team Create")
|
||||
return true
|
||||
end
|
||||
|
||||
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
||||
if not lock then
|
||||
lock = Instance.new("ObjectValue")
|
||||
lock.Name = "__Rojo_SessionLock"
|
||||
lock.Archivable = false
|
||||
lock.Value = Players.LocalPlayer
|
||||
lock.Parent = ServerStorage
|
||||
Log.trace("Created and claimed sync lock")
|
||||
return true
|
||||
end
|
||||
|
||||
if lock.Value and lock.Value ~= Players.LocalPlayer and lock.Value.Parent then
|
||||
Log.trace("Found existing sync lock owned by {}", lock.Value)
|
||||
return false, lock.Value
|
||||
end
|
||||
|
||||
lock.Value = Players.LocalPlayer
|
||||
Log.trace("Claimed existing sync lock")
|
||||
return true
|
||||
end
|
||||
|
||||
function App:releaseSyncLock()
|
||||
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
||||
if not lock then
|
||||
Log.trace("No sync lock found, assumed released")
|
||||
return
|
||||
end
|
||||
|
||||
if lock.Value == Players.LocalPlayer then
|
||||
lock.Value = nil
|
||||
Log.trace("Released sync lock")
|
||||
return
|
||||
end
|
||||
|
||||
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
||||
end
|
||||
|
||||
function App:startSession()
|
||||
local claimedLock, priorOwner = self:claimSyncLock()
|
||||
if not claimedLock then
|
||||
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
||||
|
||||
Log.warn(msg)
|
||||
self:addNotification(msg, 10)
|
||||
self:setState({
|
||||
appStatus = AppStatus.Error,
|
||||
errorMessage = msg,
|
||||
toolbarIcon = Assets.Images.PluginButtonWarning,
|
||||
})
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local host, port = self:getHostAndPort()
|
||||
|
||||
local sessionOptions = {
|
||||
openScriptsExternally = self.props.settings:get("openScriptsExternally"),
|
||||
twoWaySync = self.props.settings:get("twoWaySync"),
|
||||
openScriptsExternally = Settings:get("openScriptsExternally"),
|
||||
twoWaySync = Settings:get("twoWaySync"),
|
||||
}
|
||||
|
||||
local baseUrl = ("http://%s:%s"):format(host, port)
|
||||
local baseUrl = if string.find(host, "^https?://")
|
||||
then string.format("%s:%s", host, port)
|
||||
else string.format("http://%s:%s", host, port)
|
||||
local apiContext = ApiContext.new(baseUrl)
|
||||
|
||||
local serveSession = ServeSession.new({
|
||||
@@ -103,8 +219,37 @@ function App:startSession()
|
||||
twoWaySync = sessionOptions.twoWaySync,
|
||||
})
|
||||
|
||||
serveSession:onPatchApplied(function(patch, _unapplied)
|
||||
if PatchSet.isEmpty(patch) then
|
||||
-- Ignore empty patches
|
||||
return
|
||||
end
|
||||
|
||||
local now = os.time()
|
||||
|
||||
local old = self.patchInfo:getValue()
|
||||
if now - old.timestamp < 2 then
|
||||
-- Patches that apply in the same second are
|
||||
-- considered to be part of the same change for human clarity
|
||||
local merged = PatchSet.newEmpty()
|
||||
PatchSet.assign(merged, old.patch, patch)
|
||||
|
||||
self.setPatchInfo({
|
||||
patch = merged,
|
||||
timestamp = now,
|
||||
})
|
||||
else
|
||||
self.setPatchInfo({
|
||||
patch = patch,
|
||||
timestamp = now,
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
serveSession:onStatusChanged(function(status, details)
|
||||
if status == ServeSession.Status.Connecting then
|
||||
self:setPriorEndpoint(host, port)
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.Connecting,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
@@ -121,6 +266,7 @@ function App:startSession()
|
||||
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
||||
elseif status == ServeSession.Status.Disconnected then
|
||||
self.serveSession = nil
|
||||
self:releaseSyncLock()
|
||||
|
||||
-- Details being present indicates that this
|
||||
-- disconnection was from an error.
|
||||
@@ -143,9 +289,45 @@ function App:startSession()
|
||||
end
|
||||
end)
|
||||
|
||||
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
||||
if PatchSet.isEmpty(patch) then
|
||||
return "Accept"
|
||||
end
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.Confirming,
|
||||
confirmData = {
|
||||
instanceMap = instanceMap,
|
||||
patch = patch,
|
||||
serverInfo = serverInfo,
|
||||
},
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
|
||||
self:addNotification(
|
||||
string.format(
|
||||
"Please accept%sor abort the initializing sync session.",
|
||||
Settings:get("twoWaySync") and ", reject, " or " "
|
||||
),
|
||||
7
|
||||
)
|
||||
|
||||
return self.confirmationEvent:Wait()
|
||||
end)
|
||||
|
||||
serveSession:start()
|
||||
|
||||
self.serveSession = serveSession
|
||||
|
||||
task.defer(function()
|
||||
while self.serveSession == serveSession do
|
||||
-- Trigger rerender to update timestamp text
|
||||
local patchInfo = table.clone(self.patchInfo:getValue())
|
||||
self.setPatchInfo(patchInfo)
|
||||
local elapsed = os.time() - patchInfo.timestamp
|
||||
task.wait(elapsed < 60 and 1 or elapsed / 5)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function App:endSession()
|
||||
@@ -182,100 +364,119 @@ function App:render()
|
||||
value = self.props.plugin,
|
||||
}, {
|
||||
e(Theme.StudioProvider, nil, {
|
||||
gui = e(StudioPluginGui, {
|
||||
id = pluginName,
|
||||
title = pluginName,
|
||||
active = self.state.guiEnabled,
|
||||
e(Tooltip.Provider, nil, {
|
||||
gui = e(StudioPluginGui, {
|
||||
id = pluginName,
|
||||
title = pluginName,
|
||||
active = self.state.guiEnabled,
|
||||
|
||||
initDockState = Enum.InitialDockState.Right,
|
||||
initEnabled = false,
|
||||
overridePreviousState = false,
|
||||
floatingSize = Vector2.new(300, 200),
|
||||
minimumSize = Vector2.new(300, 200),
|
||||
initDockState = Enum.InitialDockState.Right,
|
||||
initEnabled = false,
|
||||
overridePreviousState = false,
|
||||
floatingSize = Vector2.new(300, 200),
|
||||
minimumSize = Vector2.new(300, 120),
|
||||
|
||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
|
||||
onInitialState = function(initialState)
|
||||
self:setState({
|
||||
guiEnabled = initialState,
|
||||
})
|
||||
end,
|
||||
|
||||
onClose = function()
|
||||
self:setState({
|
||||
guiEnabled = false,
|
||||
})
|
||||
end,
|
||||
}, {
|
||||
NotConnectedPage = createPageElement(AppStatus.NotConnected, {
|
||||
host = self.host,
|
||||
onHostChange = self.setHost,
|
||||
port = self.port,
|
||||
onPortChange = self.setPort,
|
||||
|
||||
onConnect = function()
|
||||
self:startSession()
|
||||
end,
|
||||
|
||||
onNavigateSettings = function()
|
||||
onInitialState = function(initialState)
|
||||
self:setState({
|
||||
appStatus = AppStatus.Settings,
|
||||
guiEnabled = initialState,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
|
||||
Connecting = createPageElement(AppStatus.Connecting),
|
||||
|
||||
Connected = createPageElement(AppStatus.Connected, {
|
||||
projectName = self.state.projectName,
|
||||
address = self.state.address,
|
||||
|
||||
onDisconnect = function()
|
||||
self:endSession()
|
||||
end,
|
||||
}),
|
||||
|
||||
Settings = createPageElement(AppStatus.Settings, {
|
||||
onBack = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
|
||||
Error = createPageElement(AppStatus.Error, {
|
||||
errorMessage = self.state.errorMessage,
|
||||
|
||||
onClose = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
guiEnabled = false,
|
||||
})
|
||||
end,
|
||||
}, {
|
||||
Tooltips = e(Tooltip.Container, nil),
|
||||
|
||||
NotConnectedPage = createPageElement(AppStatus.NotConnected, {
|
||||
host = self.host,
|
||||
onHostChange = self.setHost,
|
||||
port = self.port,
|
||||
onPortChange = self.setPort,
|
||||
|
||||
onConnect = function()
|
||||
self:startSession()
|
||||
end,
|
||||
|
||||
onNavigateSettings = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.Settings,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
|
||||
ConfirmingPage = createPageElement(AppStatus.Confirming, {
|
||||
confirmData = self.state.confirmData,
|
||||
createPopup = not self.state.guiEnabled,
|
||||
|
||||
onAbort = function()
|
||||
self.confirmationBindable:Fire("Abort")
|
||||
end,
|
||||
onAccept = function()
|
||||
self.confirmationBindable:Fire("Accept")
|
||||
end,
|
||||
onReject = function()
|
||||
self.confirmationBindable:Fire("Reject")
|
||||
end,
|
||||
}),
|
||||
|
||||
Connecting = createPageElement(AppStatus.Connecting),
|
||||
|
||||
Connected = createPageElement(AppStatus.Connected, {
|
||||
projectName = self.state.projectName,
|
||||
address = self.state.address,
|
||||
patchInfo = self.patchInfo,
|
||||
serveSession = self.serveSession,
|
||||
|
||||
onDisconnect = function()
|
||||
self:endSession()
|
||||
end,
|
||||
}),
|
||||
|
||||
Settings = createPageElement(AppStatus.Settings, {
|
||||
onBack = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
|
||||
Error = createPageElement(AppStatus.Error, {
|
||||
errorMessage = self.state.errorMessage,
|
||||
|
||||
onClose = function()
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
}),
|
||||
|
||||
Background = Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
ZIndex = 0,
|
||||
BorderSizePixel = 0,
|
||||
})
|
||||
end),
|
||||
}),
|
||||
|
||||
RojoNotifications = e("ScreenGui", {}, {
|
||||
layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
notifs = e(Notifications, {
|
||||
notifications = self.state.notifications,
|
||||
onClose = function(index)
|
||||
self:closeNotification(index)
|
||||
end,
|
||||
RojoNotifications = e("ScreenGui", {}, {
|
||||
layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 5),
|
||||
PaddingBottom = UDim.new(0, 5),
|
||||
PaddingLeft = UDim.new(0, 5),
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}),
|
||||
notifs = e(Notifications, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
notifications = self.state.notifications,
|
||||
onClose = function(index)
|
||||
self:closeNotification(index)
|
||||
end,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -288,7 +489,9 @@ function App:render()
|
||||
onTriggered = function()
|
||||
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
|
||||
self:startSession()
|
||||
elseif self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected then
|
||||
elseif
|
||||
self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected
|
||||
then
|
||||
self:endSession()
|
||||
end
|
||||
end,
|
||||
@@ -336,21 +539,16 @@ function App:render()
|
||||
}
|
||||
end)
|
||||
end,
|
||||
})
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return function(props)
|
||||
return e(PluginSettings.StudioProvider, {
|
||||
plugin = props.plugin,
|
||||
}, {
|
||||
App = PluginSettings.with(function(settings)
|
||||
local settingsProps = Dictionary.merge(props, {
|
||||
settings = settings,
|
||||
})
|
||||
return e(App, settingsProps)
|
||||
end),
|
||||
local mergedProps = Dictionary.merge(props, {
|
||||
soundPlayer = soundPlayer.new(Settings),
|
||||
})
|
||||
|
||||
return e(App, mergedProps)
|
||||
end
|
||||
|
||||
@@ -23,11 +23,20 @@ local Assets = {
|
||||
Icons = {
|
||||
Close = "rbxassetid://6012985953",
|
||||
Back = "rbxassetid://6017213752",
|
||||
Reset = "rbxassetid://10142422327",
|
||||
},
|
||||
Diff = {
|
||||
Add = "rbxassetid://10434145835",
|
||||
Remove = "rbxassetid://10434408368",
|
||||
Edit = "rbxassetid://10434144680",
|
||||
},
|
||||
Checkbox = {
|
||||
Active = "rbxassetid://6016251644",
|
||||
Inactive = "rbxassetid://6016251963",
|
||||
},
|
||||
Dropdown = {
|
||||
Arrow = "rbxassetid://10131770538",
|
||||
},
|
||||
Spinner = {
|
||||
Foreground = "rbxassetid://3222731032",
|
||||
Background = "rbxassetid://3222730627",
|
||||
@@ -46,7 +55,7 @@ local Assets = {
|
||||
},
|
||||
},
|
||||
Sounds = {
|
||||
Notification = "rbxassetid://9716079936",
|
||||
Notification = "rbxassetid://203785492",
|
||||
},
|
||||
StartSession = "",
|
||||
SessionActive = "",
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
of instances) and return the patch.
|
||||
]]
|
||||
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
local RbxDom = require(Packages.RbxDom)
|
||||
|
||||
local encodeProperty = require(script.Parent.encodeProperty)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
local RbxDom = require(Packages.RbxDom)
|
||||
|
||||
return function(instance, propertyName, propertyDescriptor)
|
||||
local readSuccess, readResult = propertyDescriptor:read(instance)
|
||||
|
||||
@@ -5,9 +5,9 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
||||
return strict("Config", {
|
||||
isDevBuild = isDevBuild,
|
||||
codename = "Epiphany",
|
||||
version = {7, 2, 0},
|
||||
expectedServerVersionString = "7.0 or newer",
|
||||
version = {7, 2, 1},
|
||||
expectedServerVersionString = "7.2 or newer",
|
||||
protocolVersion = 4,
|
||||
defaultHost = "localhost",
|
||||
defaultPort = 34872,
|
||||
})
|
||||
defaultPort = "34872",
|
||||
})
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
local Config = require(script.Parent.Config)
|
||||
|
||||
local Environment = {
|
||||
User = "User",
|
||||
Dev = "Dev",
|
||||
Test = "Test",
|
||||
}
|
||||
|
||||
local DEFAULT_ENVIRONMENT = Config.isDevBuild and Environment.Dev or Environment.User
|
||||
|
||||
local VALUES = {
|
||||
LogLevel = {
|
||||
type = "IntValue",
|
||||
values = {
|
||||
[Environment.User] = 2,
|
||||
[Environment.Dev] = 4,
|
||||
[Environment.Test] = 4,
|
||||
},
|
||||
},
|
||||
TypecheckingEnabled = {
|
||||
type = "BoolValue",
|
||||
values = {
|
||||
[Environment.User] = false,
|
||||
[Environment.Dev] = true,
|
||||
[Environment.Test] = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local CONTAINER_NAME = "RojoDevSettings" .. Config.codename
|
||||
|
||||
local function getValueContainer()
|
||||
return game:FindFirstChild(CONTAINER_NAME)
|
||||
end
|
||||
|
||||
local valueContainer = getValueContainer()
|
||||
|
||||
game.ChildAdded:Connect(function(child)
|
||||
local success, name = pcall(function()
|
||||
return child.Name
|
||||
end)
|
||||
|
||||
if success and name == CONTAINER_NAME then
|
||||
valueContainer = child
|
||||
end
|
||||
end)
|
||||
|
||||
local function getStoredValue(name)
|
||||
if valueContainer == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local valueObject = valueContainer:FindFirstChild(name)
|
||||
|
||||
if valueObject == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return valueObject.Value
|
||||
end
|
||||
|
||||
local function setStoredValue(name, kind, value)
|
||||
local object = valueContainer:FindFirstChild(name)
|
||||
|
||||
if object == nil then
|
||||
object = Instance.new(kind)
|
||||
object.Name = name
|
||||
object.Parent = valueContainer
|
||||
end
|
||||
|
||||
object.Value = value
|
||||
end
|
||||
|
||||
local function createAllValues(environment)
|
||||
assert(Environment[environment] ~= nil, "Invalid environment")
|
||||
|
||||
valueContainer = getValueContainer()
|
||||
|
||||
if valueContainer == nil then
|
||||
valueContainer = Instance.new("Folder")
|
||||
valueContainer.Name = CONTAINER_NAME
|
||||
valueContainer.Parent = game
|
||||
end
|
||||
|
||||
for name, value in pairs(VALUES) do
|
||||
setStoredValue(name, value.type, value.values[environment])
|
||||
end
|
||||
end
|
||||
|
||||
local function getValue(name)
|
||||
assert(VALUES[name] ~= nil, "Invalid DevSettings name")
|
||||
|
||||
local stored = getStoredValue(name)
|
||||
|
||||
if stored ~= nil then
|
||||
return stored
|
||||
end
|
||||
|
||||
return VALUES[name].values[DEFAULT_ENVIRONMENT]
|
||||
end
|
||||
|
||||
local DevSettings = {}
|
||||
|
||||
function DevSettings:createDevSettings()
|
||||
createAllValues(Environment.Dev)
|
||||
end
|
||||
|
||||
function DevSettings:createTestSettings()
|
||||
createAllValues(Environment.Test)
|
||||
end
|
||||
|
||||
function DevSettings:hasChangedValues()
|
||||
return valueContainer ~= nil
|
||||
end
|
||||
|
||||
function DevSettings:resetValues()
|
||||
if valueContainer then
|
||||
valueContainer:Destroy()
|
||||
valueContainer = nil
|
||||
end
|
||||
end
|
||||
|
||||
function DevSettings:isEnabled()
|
||||
return valueContainer ~= nil
|
||||
end
|
||||
|
||||
function DevSettings:getLogLevel()
|
||||
return getValue("LogLevel")
|
||||
end
|
||||
|
||||
function DevSettings:shouldTypecheck()
|
||||
return getValue("TypecheckingEnabled")
|
||||
end
|
||||
|
||||
function _G.ROJO_DEV_CREATE()
|
||||
DevSettings:createDevSettings()
|
||||
end
|
||||
|
||||
return DevSettings
|
||||
@@ -1,6 +1,7 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Log = require(script.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
--[[
|
||||
A bidirectional map between instance IDs and Roblox instances. It lets us
|
||||
|
||||
@@ -3,10 +3,45 @@
|
||||
patch returned from the API.
|
||||
]]
|
||||
|
||||
local t = require(script.Parent.Parent.t)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local t = require(Packages.t)
|
||||
|
||||
local Types = require(script.Parent.Types)
|
||||
|
||||
local function deepEqual(a: any, b: any): boolean
|
||||
local typeA = typeof(a)
|
||||
if typeA ~= typeof(b) then
|
||||
return false
|
||||
end
|
||||
|
||||
if typeof(a) == "table" then
|
||||
local checkedKeys = {}
|
||||
|
||||
for key, value in a do
|
||||
checkedKeys[key] = true
|
||||
|
||||
if deepEqual(value, b[key]) == false then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in b do
|
||||
if checkedKeys[key] then continue end
|
||||
if deepEqual(value, a[key]) == false then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if a == b then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local PatchSet = {}
|
||||
|
||||
PatchSet.validate = t.interface({
|
||||
@@ -56,6 +91,32 @@ function PatchSet.hasUpdates(patchSet)
|
||||
return next(patchSet.updated) ~= nil
|
||||
end
|
||||
|
||||
--[[
|
||||
Tells whether the given PatchSets are equal.
|
||||
]]
|
||||
function PatchSet.isEqual(patchA, patchB)
|
||||
return deepEqual(patchA, patchB)
|
||||
end
|
||||
|
||||
--[[
|
||||
Count the number of changes in the given PatchSet.
|
||||
]]
|
||||
function PatchSet.countChanges(patch)
|
||||
local count = 0
|
||||
|
||||
for _ in patch.added do
|
||||
count += 1
|
||||
end
|
||||
for _ in patch.removed do
|
||||
count += 1
|
||||
end
|
||||
for _ in patch.updated do
|
||||
count += 1
|
||||
end
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
--[[
|
||||
Merge multiple PatchSet objects into the given PatchSet.
|
||||
]]
|
||||
@@ -181,4 +242,4 @@ function PatchSet.humanSummary(instanceMap, patchSet)
|
||||
return table.concat(statements, "\n")
|
||||
end
|
||||
|
||||
return PatchSet
|
||||
return PatchSet
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
Defines the errors that can be returned by the reconciler.
|
||||
]]
|
||||
|
||||
local Fmt = require(script.Parent.Parent.Parent.Fmt)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Fmt = require(Packages.Fmt)
|
||||
|
||||
local Error = {}
|
||||
|
||||
@@ -34,4 +35,4 @@ function Error:__tostring()
|
||||
return Fmt.fmt("Error({}): {:#?}", self.kind, self.details)
|
||||
end
|
||||
|
||||
return Error
|
||||
return Error
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
Patches can come from the server or be generated by the client.
|
||||
]]
|
||||
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
local Types = require(script.Parent.Parent.Types)
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
usable by Rojo's reconciler, potentially using RbxDom.
|
||||
]]
|
||||
|
||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local RbxDom = require(Packages.RbxDom)
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
local function decodeValue(encodedValue, instanceMap)
|
||||
@@ -38,4 +39,4 @@ local function decodeValue(encodedValue, instanceMap)
|
||||
return true, decodedValue
|
||||
end
|
||||
|
||||
return decodeValue
|
||||
return decodeValue
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
patch that can be later applied.
|
||||
]]
|
||||
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local invariant = require(script.Parent.Parent.invariant)
|
||||
local getProperty = require(script.Parent.getProperty)
|
||||
local Error = require(script.Parent.Error)
|
||||
@@ -13,6 +15,86 @@ local function isEmpty(table)
|
||||
return next(table) == nil
|
||||
end
|
||||
|
||||
local function fuzzyEq(a: number, b: number, epsilon: number): boolean
|
||||
return math.abs(a - b) < epsilon
|
||||
end
|
||||
|
||||
local function trueEquals(a, b): boolean
|
||||
-- Exit early for simple equality values
|
||||
if a == b then
|
||||
return true
|
||||
end
|
||||
|
||||
local typeA, typeB = typeof(a), typeof(b)
|
||||
|
||||
-- For tables, try recursive deep equality
|
||||
if typeA == "table" and typeB == "table" then
|
||||
local checkedKeys = {}
|
||||
for key, value in pairs(a) do
|
||||
checkedKeys[key] = true
|
||||
if not trueEquals(value, b[key]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, value in pairs(b) do
|
||||
if checkedKeys[key] then continue end
|
||||
if not trueEquals(value, a[key]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
|
||||
-- For numbers, compare with epsilon of 0.0001 to avoid floating point inequality
|
||||
elseif typeA == "number" and typeB == "number" then
|
||||
return fuzzyEq(a, b, 0.0001)
|
||||
|
||||
-- For EnumItem->number, compare the EnumItem's value
|
||||
elseif typeA == "number" and typeB == "EnumItem" then
|
||||
return a == b.Value
|
||||
elseif typeA == "EnumItem" and typeB == "number" then
|
||||
return a.Value == b
|
||||
|
||||
-- For Color3s, compare to RGB ints to avoid floating point inequality
|
||||
elseif typeA == "Color3" and typeB == "Color3" then
|
||||
local aR, aG, aB = math.floor(a.R * 255), math.floor(a.G * 255), math.floor(a.B * 255)
|
||||
local bR, bG, bB = math.floor(b.R * 255), math.floor(b.G * 255), math.floor(b.B * 255)
|
||||
return aR == bR and aG == bG and aB == bB
|
||||
|
||||
-- For CFrames, compare to components with epsilon of 0.0001 to avoid floating point inequality
|
||||
elseif typeA == "CFrame" and typeB == "CFrame" then
|
||||
local aComponents, bComponents = {a:GetComponents()}, {b:GetComponents()}
|
||||
for i, aComponent in aComponents do
|
||||
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
|
||||
-- For Vector3s, compare to components with epsilon of 0.0001 to avoid floating point inequality
|
||||
elseif typeA == "Vector3" and typeB == "Vector3" then
|
||||
local aComponents, bComponents = {a.X, a.Y, a.Z}, {b.X, b.Y, b.Z}
|
||||
for i, aComponent in aComponents do
|
||||
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
|
||||
-- For Vector2s, compare to components with epsilon of 0.0001 to avoid floating point inequality
|
||||
elseif typeA == "Vector2" and typeB == "Vector2" then
|
||||
local aComponents, bComponents = {a.X, a.Y}, {b.X, b.Y}
|
||||
for i, aComponent in aComponents do
|
||||
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function shouldDeleteUnknownInstances(virtualInstance)
|
||||
if virtualInstance.Metadata ~= nil then
|
||||
return not virtualInstance.Metadata.ignoreUnknownInstances
|
||||
@@ -71,7 +153,8 @@ local function diff(instanceMap, virtualInstances, rootId)
|
||||
local ok, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||
|
||||
if ok then
|
||||
if existingValue ~= decodedValue then
|
||||
if not trueEquals(existingValue, decodedValue) then
|
||||
Log.debug("{}.{} changed from '{}' to '{}'", instance:GetFullName(), propertyName, existingValue, decodedValue)
|
||||
changedProperties[propertyName] = virtualValue
|
||||
end
|
||||
else
|
||||
@@ -113,6 +196,16 @@ local function diff(instanceMap, virtualInstances, rootId)
|
||||
local childId = instanceMap.fromInstances[childInstance]
|
||||
|
||||
if childId == nil then
|
||||
-- pcall to avoid security permission errors
|
||||
local success, skip = pcall(function()
|
||||
-- We don't remove instances that aren't going to be saved anyway,
|
||||
-- such as the Rojo session lock value.
|
||||
return childInstance.Archivable == false
|
||||
end)
|
||||
if success and skip then
|
||||
continue
|
||||
end
|
||||
|
||||
-- This is an existing instance not present in the virtual DOM.
|
||||
-- We can mark it for deletion unless the user has asked us not
|
||||
-- to delete unknown stuff.
|
||||
@@ -152,4 +245,4 @@ local function diff(instanceMap, virtualInstances, rootId)
|
||||
return true, patch
|
||||
end
|
||||
|
||||
return diff
|
||||
return diff
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
return function()
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
@@ -286,4 +287,4 @@ return function()
|
||||
expect(size(patch.added)).to.equal(1)
|
||||
expect(patch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
Attempts to read a property from the given instance.
|
||||
]]
|
||||
|
||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local RbxDom = require(Packages.RbxDom)
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
local function getProperty(instance, propertyName)
|
||||
@@ -56,4 +57,4 @@ local function getProperty(instance, propertyName)
|
||||
return true, valueOrErr
|
||||
end
|
||||
|
||||
return getProperty
|
||||
return getProperty
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
Attempts to set a property on the given instance.
|
||||
]]
|
||||
|
||||
local RbxDom = require(script.Parent.Parent.Parent.RbxDom)
|
||||
local Log = require(script.Parent.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
local RbxDom = require(Packages.RbxDom)
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
local function setProperty(instance, propertyName, value)
|
||||
@@ -45,4 +46,4 @@ local function setProperty(instance, propertyName, value)
|
||||
return true
|
||||
end
|
||||
|
||||
return setProperty
|
||||
return setProperty
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Log = require(script.Parent.Parent.Log)
|
||||
local Fmt = require(script.Parent.Parent.Fmt)
|
||||
local t = require(script.Parent.Parent.t)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
local Fmt = require(Packages.Fmt)
|
||||
local t = require(Packages.t)
|
||||
local Promise = require(Packages.Promise)
|
||||
|
||||
local ChangeBatcher = require(script.Parent.ChangeBatcher)
|
||||
local encodePatchUpdate = require(script.Parent.ChangeBatcher.encodePatchUpdate)
|
||||
local InstanceMap = require(script.Parent.InstanceMap)
|
||||
local PatchSet = require(script.Parent.PatchSet)
|
||||
local Reconciler = require(script.Parent.Reconciler)
|
||||
@@ -94,6 +97,7 @@ function ServeSession.new(options)
|
||||
__instanceMap = instanceMap,
|
||||
__changeBatcher = changeBatcher,
|
||||
__statusChangedCallback = nil,
|
||||
__patchAppliedCallback = nil,
|
||||
__connections = connections,
|
||||
}
|
||||
|
||||
@@ -121,23 +125,32 @@ function ServeSession:onStatusChanged(callback)
|
||||
self.__statusChangedCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:setConfirmCallback(callback)
|
||||
self.__userConfirmCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:onPatchApplied(callback)
|
||||
self.__patchAppliedCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:start()
|
||||
self:__setStatus(Status.Connecting)
|
||||
|
||||
self.__apiContext:connect()
|
||||
:andThen(function(serverInfo)
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
local rootInstanceId = serverInfo.rootInstanceId
|
||||
|
||||
return self:__initialSync(rootInstanceId)
|
||||
return self:__initialSync(serverInfo)
|
||||
:andThen(function()
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
|
||||
return self:__mainSyncLoop()
|
||||
end)
|
||||
end)
|
||||
:catch(function(err)
|
||||
self:__stopInternal(err)
|
||||
if self.__status ~= Status.Disconnected then
|
||||
self:__stopInternal(err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -194,8 +207,8 @@ function ServeSession:__onActiveScriptChanged(activeScript)
|
||||
self.__apiContext:open(scriptId)
|
||||
end
|
||||
|
||||
function ServeSession:__initialSync(rootInstanceId)
|
||||
return self.__apiContext:read({ rootInstanceId })
|
||||
function ServeSession:__initialSync(serverInfo)
|
||||
return self.__apiContext:read({ serverInfo.rootInstanceId })
|
||||
:andThen(function(readResponseBody)
|
||||
-- Tell the API Context that we're up-to-date with the version of
|
||||
-- the tree defined in this response.
|
||||
@@ -204,14 +217,14 @@ function ServeSession:__initialSync(rootInstanceId)
|
||||
-- For any instances that line up with the Rojo server's view, start
|
||||
-- tracking them in the reconciler.
|
||||
Log.trace("Matching existing Roblox instances to Rojo IDs")
|
||||
self.__reconciler:hydrate(readResponseBody.instances, rootInstanceId, game)
|
||||
self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game)
|
||||
|
||||
-- Calculate the initial patch to apply to the DataModel to catch us
|
||||
-- up to what Rojo thinks the place should look like.
|
||||
Log.trace("Computing changes that plugin needs to make to catch up to server...")
|
||||
local success, catchUpPatch = self.__reconciler:diff(
|
||||
readResponseBody.instances,
|
||||
rootInstanceId,
|
||||
serverInfo.rootInstanceId,
|
||||
game
|
||||
)
|
||||
|
||||
@@ -221,15 +234,50 @@ function ServeSession:__initialSync(rootInstanceId)
|
||||
|
||||
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
|
||||
|
||||
-- TODO: Prompt user to notify them of this patch, since it's
|
||||
-- effectively a conflict between the Rojo server and the client. In
|
||||
-- the future, we'll ask which changes the user wants to keep.
|
||||
local userDecision = "Accept"
|
||||
if self.__userConfirmCallback ~= nil then
|
||||
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo)
|
||||
end
|
||||
|
||||
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
|
||||
if userDecision == "Abort" then
|
||||
return Promise.reject("Aborted Rojo sync operation")
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
||||
elseif userDecision == "Reject" and self.__twoWaySync then
|
||||
-- The user wants their studio DOM to write back to their Rojo DOM
|
||||
-- so we will reverse the patch and send it back
|
||||
|
||||
local inversePatch = PatchSet.newEmpty()
|
||||
|
||||
-- Send back the current properties
|
||||
for _, change in catchUpPatch.updated do
|
||||
local instance = self.__instanceMap.fromIds[change.id]
|
||||
if not instance then continue end
|
||||
|
||||
local update = encodePatchUpdate(instance, change.id, change.changedProperties)
|
||||
table.insert(inversePatch.updated, update)
|
||||
end
|
||||
-- Add the removed instances back to Rojo
|
||||
-- selene:allow(empty_if, unused_variable)
|
||||
for _, instance in catchUpPatch.removed do
|
||||
-- TODO: Generate ID for our instance and add it to inversePatch.added
|
||||
end
|
||||
-- Remove the additions we've rejected
|
||||
for id, _change in catchUpPatch.added do
|
||||
table.insert(inversePatch.removed, id)
|
||||
end
|
||||
|
||||
self.__apiContext:write(inversePatch)
|
||||
|
||||
elseif userDecision == "Accept" then
|
||||
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
||||
end
|
||||
if self.__patchAppliedCallback then
|
||||
pcall(self.__patchAppliedCallback, catchUpPatch, unappliedPatch)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -244,6 +292,10 @@ function ServeSession:__mainSyncLoop()
|
||||
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
|
||||
end
|
||||
|
||||
if self.__patchAppliedCallback then
|
||||
pcall(self.__patchAppliedCallback, message, unappliedPatch)
|
||||
end
|
||||
end
|
||||
|
||||
if self.__status ~= Status.Disconnected then
|
||||
|
||||
104
plugin/src/Settings.lua
Normal file
104
plugin/src/Settings.lua
Normal file
@@ -0,0 +1,104 @@
|
||||
--[[
|
||||
Persistent plugin settings.
|
||||
]]
|
||||
|
||||
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Log = require(Packages.Log)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
showNotifications = true,
|
||||
playSounds = true,
|
||||
typecheckingEnabled = false,
|
||||
logLevel = "Info",
|
||||
priorEndpoints = {},
|
||||
}
|
||||
|
||||
local Settings = {}
|
||||
|
||||
Settings._values = table.clone(defaultSettings)
|
||||
Settings._updateListeners = {}
|
||||
Settings._bindings = {}
|
||||
|
||||
if plugin then
|
||||
for name, defaultValue in pairs(Settings._values) do
|
||||
local savedValue = plugin:GetSetting("Rojo_" .. name)
|
||||
|
||||
if savedValue == nil then
|
||||
-- plugin:SetSetting hits disc instead of memory, so it can be slow. Spawn so we don't hang.
|
||||
task.spawn(plugin.SetSetting, plugin, "Rojo_" .. name, defaultValue)
|
||||
Settings._values[name] = defaultValue
|
||||
else
|
||||
Settings._values[name] = savedValue
|
||||
end
|
||||
end
|
||||
Log.trace("Loaded settings from plugin store")
|
||||
end
|
||||
|
||||
function Settings:get(name)
|
||||
if defaultSettings[name] == nil then
|
||||
error("Invalid setings name " .. tostring(name), 2)
|
||||
end
|
||||
|
||||
return self._values[name]
|
||||
end
|
||||
|
||||
function Settings:set(name, value)
|
||||
self._values[name] = value
|
||||
if self._bindings[name] then
|
||||
self._bindings[name].set(value)
|
||||
end
|
||||
|
||||
if plugin then
|
||||
-- plugin:SetSetting hits disc instead of memory, so it can be slow. Spawn so we don't hang.
|
||||
task.spawn(plugin.SetSetting, plugin, "Rojo_" .. name, value)
|
||||
end
|
||||
|
||||
if self._updateListeners[name] then
|
||||
for callback in pairs(self._updateListeners[name]) do
|
||||
task.spawn(callback, value)
|
||||
end
|
||||
end
|
||||
|
||||
Log.trace(string.format("Set setting '%s' to '%s'", name, tostring(value)))
|
||||
end
|
||||
|
||||
function Settings:onChanged(name, callback)
|
||||
local listeners = self._updateListeners[name]
|
||||
if listeners == nil then
|
||||
listeners = {}
|
||||
self._updateListeners[name] = listeners
|
||||
end
|
||||
listeners[callback] = true
|
||||
|
||||
Log.trace(string.format("Added listener for setting '%s' changes", name))
|
||||
|
||||
return function()
|
||||
listeners[callback] = nil
|
||||
Log.trace(string.format("Removed listener for setting '%s' changes", name))
|
||||
end
|
||||
end
|
||||
|
||||
function Settings:getBinding(name)
|
||||
local cached = self._bindings[name]
|
||||
if cached then
|
||||
return cached.bind
|
||||
end
|
||||
|
||||
local bind, set = Roact.createBinding(self._values[name])
|
||||
self._bindings[name] = {
|
||||
bind = bind,
|
||||
set = set,
|
||||
}
|
||||
|
||||
Log.trace(string.format("Created binding for setting '%s'", name))
|
||||
|
||||
return bind
|
||||
end
|
||||
|
||||
return Settings
|
||||
@@ -1,6 +1,6 @@
|
||||
local t = require(script.Parent.Parent.t)
|
||||
|
||||
local DevSettings = require(script.Parent.DevSettings)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local t = require(Packages.t)
|
||||
local Settings = require(script.Parent.Settings)
|
||||
local strict = require(script.Parent.strict)
|
||||
|
||||
local RbxId = t.string
|
||||
@@ -66,7 +66,7 @@ local ApiError = t.interface({
|
||||
|
||||
local function ifEnabled(innerCheck)
|
||||
return function(...)
|
||||
if DevSettings:shouldTypecheck() then
|
||||
if Settings:get("typecheckingEnabled") then
|
||||
return innerCheck(...)
|
||||
else
|
||||
return true
|
||||
|
||||
@@ -2,19 +2,20 @@ if not plugin then
|
||||
return
|
||||
end
|
||||
|
||||
local Log = require(script.Parent.Log)
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local DevSettings = require(script.DevSettings)
|
||||
|
||||
Log.setLogLevelThunk(function()
|
||||
return DevSettings:getLogLevel()
|
||||
end)
|
||||
|
||||
local Roact = require(script.Parent.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Settings = require(script.Settings)
|
||||
local Config = require(script.Config)
|
||||
local App = require(script.App)
|
||||
|
||||
Log.setLogLevelThunk(function()
|
||||
return Log.Level[Settings:get("logLevel")] or Log.Level.Info
|
||||
end)
|
||||
|
||||
local app = Roact.createElement(App, {
|
||||
plugin = plugin
|
||||
})
|
||||
|
||||
@@ -2,7 +2,10 @@ return function()
|
||||
it("should load all submodules", function()
|
||||
local function loadRecursive(container)
|
||||
if container:IsA("ModuleScript") and not container.Name:find("%.spec$") then
|
||||
require(container)
|
||||
local success, err = pcall(require, container)
|
||||
if not success then
|
||||
error(string.format("Failed to load '%s': %s", container.Name, err))
|
||||
end
|
||||
end
|
||||
|
||||
for _, child in ipairs(container:GetChildren()) do
|
||||
@@ -12,4 +15,4 @@ return function()
|
||||
|
||||
loadRecursive(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
local Fmt = require(script.Parent.Parent.Fmt)
|
||||
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Fmt = require(Packages.Fmt)
|
||||
|
||||
local Config = require(script.Parent.Config)
|
||||
|
||||
@@ -26,4 +28,4 @@ else
|
||||
end
|
||||
end
|
||||
|
||||
return invariant
|
||||
return invariant
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- Roblox decided that sounds only play in Edit mode when parented to a plugin widget, for some reason
|
||||
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
|
||||
local widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
|
||||
Enum.InitialDockState.Float,
|
||||
false, true,
|
||||
10, 10,
|
||||
10, 10
|
||||
))
|
||||
widget.Name = "Rojo_soundPlayer"
|
||||
widget.Title = "Rojo Sound Player"
|
||||
|
||||
return function(soundId)
|
||||
local sound = Instance.new("Sound")
|
||||
sound.SoundId = soundId
|
||||
sound.Parent = widget
|
||||
|
||||
sound.Ended:Connect(function()
|
||||
sound:Destroy()
|
||||
end)
|
||||
|
||||
sound:Play()
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
local Log = require(script.Parent.Parent.Log)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Assets = require(script.Parent.Assets)
|
||||
|
||||
@@ -29,4 +30,4 @@ local function preloadAssets()
|
||||
end)()
|
||||
end
|
||||
|
||||
return preloadAssets
|
||||
return preloadAssets
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
return function(TestEZ)
|
||||
local Rojo = script.Parent.Parent
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
TestEZ.TestBootstrap:run({ Rojo.Plugin, Rojo.Http, Rojo.Log })
|
||||
end
|
||||
TestEZ.TestBootstrap:run({ Rojo.Plugin, Packages.Http, Packages.Log, Packages.RbxDom })
|
||||
end
|
||||
|
||||
38
plugin/src/soundPlayer.lua
Normal file
38
plugin/src/soundPlayer.lua
Normal file
@@ -0,0 +1,38 @@
|
||||
-- Sounds only play in Edit mode when parented to a plugin widget, for some reason
|
||||
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
|
||||
local widget = nil
|
||||
if plugin then
|
||||
widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new(
|
||||
Enum.InitialDockState.Float,
|
||||
false, true,
|
||||
10, 10,
|
||||
10, 10
|
||||
))
|
||||
widget.Name = "Rojo_soundPlayer"
|
||||
widget.Title = "Rojo Sound Player"
|
||||
end
|
||||
|
||||
local SoundPlayer = {}
|
||||
SoundPlayer.__index = SoundPlayer
|
||||
|
||||
function SoundPlayer.new(settings)
|
||||
return setmetatable({
|
||||
settings = settings,
|
||||
}, SoundPlayer)
|
||||
end
|
||||
|
||||
function SoundPlayer:play(soundId)
|
||||
if self.settings and self.settings:get("playSounds") == false then return end
|
||||
|
||||
local sound = Instance.new("Sound")
|
||||
sound.SoundId = soundId
|
||||
sound.Parent = widget
|
||||
|
||||
sound.Ended:Connect(function()
|
||||
sound:Destroy()
|
||||
end)
|
||||
|
||||
sound:Play()
|
||||
end
|
||||
|
||||
return SoundPlayer
|
||||
@@ -8,8 +8,8 @@
|
||||
"$path": "default.project.json"
|
||||
},
|
||||
|
||||
"TestEZ": {
|
||||
"$path": "modules/testez"
|
||||
"Packages": {
|
||||
"$path": "DevPackages"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
33
plugin/wally.lock
Normal file
33
plugin/wally.lock
Normal file
@@ -0,0 +1,33 @@
|
||||
# This file is automatically @generated by Wally.
|
||||
# It is not intended for manual editing.
|
||||
registry = "test"
|
||||
|
||||
[[package]]
|
||||
name = "evaera/promise"
|
||||
version = "4.0.0"
|
||||
dependencies = []
|
||||
|
||||
[[package]]
|
||||
name = "osyrisrblx/t"
|
||||
version = "3.0.0"
|
||||
dependencies = []
|
||||
|
||||
[[package]]
|
||||
name = "reselim/flipper"
|
||||
version = "2.0.0"
|
||||
dependencies = []
|
||||
|
||||
[[package]]
|
||||
name = "roblox/roact"
|
||||
version = "1.4.4"
|
||||
dependencies = []
|
||||
|
||||
[[package]]
|
||||
name = "roblox/testez"
|
||||
version = "0.4.1"
|
||||
dependencies = []
|
||||
|
||||
[[package]]
|
||||
name = "rojo-rbx/rojo"
|
||||
version = "7.2.1"
|
||||
dependencies = [["Flipper", "reselim/flipper@2.0.0"], ["Promise", "evaera/promise@4.0.0"], ["Roact", "roblox/roact@1.4.4"], ["t", "osyrisrblx/t@3.0.0"]]
|
||||
17
plugin/wally.toml
Normal file
17
plugin/wally.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "rojo-rbx/rojo"
|
||||
description = "Rojo enables Roblox developers to use professional-grade software engineering tools"
|
||||
version = "7.2.1"
|
||||
license = "MPL-2.0"
|
||||
authors = ["LPGhatguy (https://lpg.space/)"]
|
||||
registry = "https://github.com/upliftgames/wally-index"
|
||||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
Flipper = "reselim/flipper@2.0.0"
|
||||
Promise = "evaera/promise@4.0.0"
|
||||
Roact = "roblox/roact@1.4.4"
|
||||
t = "osyrisrblx/t@3.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
TestEZ = "roblox/testez@0.4.1"
|
||||
2
plugin/watch-build.sh
Normal file
2
plugin/watch-build.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
# Continously build the rojo plugin into the local plugin directory on Windows
|
||||
rojo build plugin/default.project.json -o $LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm --watch
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 99
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
@@ -11,12 +10,24 @@ expression: contents
|
||||
<Item class="Folder" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">Explicit</string>
|
||||
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString>
|
||||
<BinaryString name="AttributesSerialize">DQAAAAQAAABCb29sAwEKAAAAQnJpY2tDb2xvcg4BAAAABgAAAENvbG9yMw8AAAAAAAAAAAAAAAANAAAAQ29sb3JTZXF1ZW5jZRkCAAAAAAAAAAAAAAAAAIA/AACAPwAAgD8AAAAAAACAPwAAgD8AAIA/AACAPwcAAABGbG9hdDMyBQAAAAAHAAAARmxvYXQ2NAYAAAAAAAAAAAsAAABOdW1iZXJSYW5nZRsAAAAAAAAAAA4AAABOdW1iZXJTZXF1ZW5jZRcCAAAAAAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAABAAAAFJlY3QcAAAAAAAAAAAAAAAAAAAAAAQAAABVRGltCQAAAAAAAAAABQAAAFVEaW0yCgAAAAAAAAAAAAAAAAAAAAAHAAAAVmVjdG9yMhAAAAAAAAAAAAcAAABWZWN0b3IzEQAAAAAAAAAAAAAAAA==</BinaryString>
|
||||
</Properties>
|
||||
</Item>
|
||||
<Item class="Folder" referent="2">
|
||||
<Properties>
|
||||
<string name="Name">ImplicitAttributes</string>
|
||||
<string name="Name">Implicit</string>
|
||||
<BinaryString name="AttributesSerialize">AwAAAAQAAABCb29sAwEGAAAATnVtYmVyBgAAAAAAAOA/BgAAAFN0cmluZwIEAAAAVGVzdA==</BinaryString>
|
||||
</Properties>
|
||||
</Item>
|
||||
<Item class="Folder" referent="3">
|
||||
<Properties>
|
||||
<string name="Name">LegacyExplicit</string>
|
||||
<BinaryString name="AttributesSerialize">AgAAAAUAAABIZWxsbwIFAAAAV29ybGQGAAAAVmVjdG9yEQAAgD8AAABAAABAQA==</BinaryString>
|
||||
</Properties>
|
||||
</Item>
|
||||
<Item class="Folder" referent="4">
|
||||
<Properties>
|
||||
<string name="Name">LegacyImplicit</string>
|
||||
<BinaryString name="AttributesSerialize">AgAAAAMAAABIZXkCBwAAAEdyYW5kbWEGAAAAVmVjdG9yEQAAgEAAAKBAAADAQA==</BinaryString>
|
||||
</Properties>
|
||||
</Item>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 100
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="LocalizationTable" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">init_csv_with_children</string>
|
||||
<string name="Contents">[{"key":"init.csv","values":{}}]</string>
|
||||
</Properties>
|
||||
<Item class="LocalizationTable" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">other</string>
|
||||
<string name="Contents">[{"key":"other.csv","values":{}}]</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
@@ -3,7 +3,85 @@
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
|
||||
"Implicit": {
|
||||
"$className": "Folder",
|
||||
"$attributes": {
|
||||
"Bool": true,
|
||||
"Number": 0.5,
|
||||
"String": "Test"
|
||||
}
|
||||
},
|
||||
|
||||
"Explicit": {
|
||||
"$className": "Folder",
|
||||
"$attributes": {
|
||||
"Bool": {
|
||||
"Bool": true
|
||||
},
|
||||
"Float32": {
|
||||
"Float32": 0
|
||||
},
|
||||
"Float64": {
|
||||
"Float64": 0
|
||||
},
|
||||
"UDim": {
|
||||
"UDim": [0, 0]
|
||||
},
|
||||
"UDim2": {
|
||||
"UDim2": [[0, 0], [0, 0]]
|
||||
},
|
||||
"BrickColor": {
|
||||
"BrickColor": 1
|
||||
},
|
||||
"Color3": {
|
||||
"Color3": [0, 0, 0]
|
||||
},
|
||||
"Vector2": {
|
||||
"Vector2": [0, 0]
|
||||
},
|
||||
"Vector3": {
|
||||
"Vector3": [0, 0, 0]
|
||||
},
|
||||
"NumberSequence": {
|
||||
"NumberSequence": {
|
||||
"keypoints": [
|
||||
{
|
||||
"time": 0,
|
||||
"value": 0,
|
||||
"envelope": 0
|
||||
},
|
||||
{
|
||||
"time": 1,
|
||||
"value": 0,
|
||||
"envelope": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ColorSequence": {
|
||||
"ColorSequence": {
|
||||
"keypoints": [
|
||||
{
|
||||
"time": 0,
|
||||
"color": [1, 1, 1]
|
||||
},
|
||||
{
|
||||
"time": 1,
|
||||
"color": [1, 1, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"NumberRange": {
|
||||
"NumberRange": [0, 0]
|
||||
},
|
||||
"Rect": {
|
||||
"Rect": [[0, 0], [0, 0]]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"LegacyExplicit": {
|
||||
"$className": "Folder",
|
||||
"$properties": {
|
||||
"Attributes": {
|
||||
@@ -19,7 +97,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
"ImplicitAttributes": {
|
||||
"LegacyImplicit": {
|
||||
"$className": "Folder",
|
||||
"$properties": {
|
||||
"Attributes": {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "init_csv_with_children",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Key
|
||||
init.csv
|
||||
|
@@ -0,0 +1,2 @@
|
||||
Key
|
||||
other.csv
|
||||
|
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
@@ -124,44 +125,20 @@ impl JobThreadContext {
|
||||
|
||||
// For a given VFS event, we might have many changes to different parts
|
||||
// of the tree. Calculate and apply all of these changes.
|
||||
let applied_patches = {
|
||||
let mut tree = self.tree.lock().unwrap();
|
||||
let mut applied_patches = Vec::new();
|
||||
|
||||
match event {
|
||||
VfsEvent::Create(path) | VfsEvent::Write(path) | VfsEvent::Remove(path) => {
|
||||
// Find the nearest ancestor to this path that has
|
||||
// associated instances in the tree. This helps make sure
|
||||
// that we handle additions correctly, especially if we
|
||||
// receive events for descendants of a large tree being
|
||||
// created all at once.
|
||||
let mut current_path = path.as_path();
|
||||
let affected_ids = loop {
|
||||
let ids = tree.get_ids_at_path(¤t_path);
|
||||
|
||||
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
||||
|
||||
if !ids.is_empty() {
|
||||
break ids.to_vec();
|
||||
}
|
||||
|
||||
log::trace!("Trying parent path...");
|
||||
match current_path.parent() {
|
||||
Some(parent) => current_path = parent,
|
||||
None => break Vec::new(),
|
||||
}
|
||||
};
|
||||
|
||||
for id in affected_ids {
|
||||
if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) {
|
||||
applied_patches.push(patch);
|
||||
}
|
||||
}
|
||||
let applied_patches = match event {
|
||||
VfsEvent::Write(path) => {
|
||||
if path.is_dir() {
|
||||
return;
|
||||
}
|
||||
_ => log::warn!("Unhandled VFS event: {:?}", event),
|
||||
on_vfs_event(path, &self.tree, &self.vfs)
|
||||
}
|
||||
VfsEvent::Create(path) | VfsEvent::Remove(path) => {
|
||||
on_vfs_event(path, &self.tree, &self.vfs)
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Unhandled VFS event: {:?}", event);
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
applied_patches
|
||||
};
|
||||
|
||||
// Notify anyone listening to the message queue about the changes we
|
||||
@@ -253,10 +230,51 @@ impl JobThreadContext {
|
||||
apply_patch_set(&mut tree, patch_set)
|
||||
};
|
||||
|
||||
self.message_queue.push_messages(&[applied_patch]);
|
||||
if !applied_patch.is_empty() {
|
||||
self.message_queue.push_messages(&[applied_patch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the nearest ancestor to this path that has
|
||||
// associated instances in the tree. This helps make sure
|
||||
// that we handle additions correctly, especially if we
|
||||
// receive events for descendants of a large tree being
|
||||
// created all at once.
|
||||
fn on_vfs_event(
|
||||
path: PathBuf,
|
||||
tree: &Arc<Mutex<RojoTree>>,
|
||||
vfs: &Arc<Vfs>,
|
||||
) -> Vec<AppliedPatchSet> {
|
||||
let mut tree = tree.lock().unwrap();
|
||||
let mut applied_patches = Vec::new();
|
||||
|
||||
let mut current_path = path.as_path();
|
||||
let affected_ids = loop {
|
||||
let ids = tree.get_ids_at_path(¤t_path);
|
||||
|
||||
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
||||
|
||||
if !ids.is_empty() {
|
||||
break ids.to_vec();
|
||||
}
|
||||
|
||||
log::trace!("Trying parent path...");
|
||||
match current_path.parent() {
|
||||
Some(parent) => current_path = parent,
|
||||
None => break Vec::new(),
|
||||
}
|
||||
};
|
||||
|
||||
for id in affected_ids {
|
||||
if let Some(patch) = compute_and_apply_changes(&mut tree, &vfs, id) {
|
||||
if !patch.is_empty() {
|
||||
applied_patches.push(patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
applied_patches
|
||||
}
|
||||
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<AppliedPatchSet> {
|
||||
let metadata = tree
|
||||
.get_metadata(id)
|
||||
|
||||
@@ -5,6 +5,8 @@ use clap::Parser;
|
||||
|
||||
use crate::project::Project;
|
||||
|
||||
use super::resolve_path;
|
||||
|
||||
/// Reformat a Rojo project using the standard JSON formatting rules.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FmtProjectCommand {
|
||||
@@ -15,7 +17,8 @@ pub struct FmtProjectCommand {
|
||||
|
||||
impl FmtProjectCommand {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
let project = Project::load_fuzzy(&self.project)?
|
||||
let base_path = resolve_path(&self.project);
|
||||
let project = Project::load_fuzzy(&base_path)?
|
||||
.context("A project file is required to run 'rojo fmt-project'")?;
|
||||
|
||||
let serialized = serde_json::to_string_pretty(&project)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user