mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 20:55:50 +00:00
Compare commits
71 Commits
aarch-wind
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
c552fdc52e
|
|||
|
|
02b41133f8 | ||
|
|
d08780fc14 | ||
|
|
b89cc7f398 | ||
|
|
42568b9709 | ||
|
|
87f58e0a55 | ||
|
|
a61a1bef55 | ||
|
|
a99e877b7c | ||
|
|
93e9c51204 | ||
|
|
015b5bda14 | ||
|
|
2b47861a4f | ||
|
|
9b5a07191b | ||
|
|
071b6e7e23 | ||
|
|
31ec216a95 | ||
|
|
ea70d89291 | ||
|
|
03410ced6d | ||
|
|
825726c883 | ||
|
|
54e63d88d4 | ||
|
|
4018c97cb6 | ||
|
|
d0b029f995 | ||
|
|
aabe6d11b2 | ||
|
|
181cc37744 | ||
|
|
cd78f5c02c | ||
|
|
441c469966 | ||
|
|
f3c423d77d | ||
|
|
beb497878b | ||
|
|
6ea95d487c | ||
|
|
80a381dbb1 | ||
|
|
59e36491a5 | ||
|
|
c1326ba06e | ||
|
|
e2633126ee | ||
|
|
5f33435f3c | ||
|
|
54e0ff230b | ||
|
|
4e9e6233ff | ||
|
|
0056849b51 | ||
|
|
2ddb21ec5f | ||
|
|
a4eb65ca3f | ||
|
|
3002d250a1 | ||
|
|
9598553e5d | ||
|
|
7f68d9887b | ||
|
|
e092a7301f | ||
|
|
6dfdfbe514 | ||
|
|
7860f2717f | ||
|
|
60f19df9a0 | ||
|
|
951f0cda0b | ||
|
|
227042d6b1 | ||
|
|
b2c4f550ee | ||
|
|
4ddbefa88f | ||
|
|
d935115591 | ||
|
|
bd2ea42732 | ||
|
|
3bac38ee34 | ||
|
|
a7a4f6d8f2 | ||
|
|
80b6facbd3 | ||
|
|
7dee898400 | ||
|
|
4c4b2dbe17 | ||
|
|
73ed5ae697 | ||
|
|
833320de64 | ||
|
|
0d6ff8ef8a | ||
|
|
55a207a275 | ||
|
|
f33d1f1cc4 | ||
|
|
19ca2b12fc | ||
|
|
b7d3394464 | ||
|
|
8c33100d7a | ||
|
|
80c406f196 | ||
|
|
bc2c76e5e2 | ||
|
|
4a7bddbc09 | ||
|
|
e316fdbaef | ||
|
|
34106f470f | ||
|
|
d9ab0e7de8 | ||
|
|
5ca1573e2e | ||
|
|
c9ce996626 |
2
.dir-locals.el
Normal file
2
.dir-locals.el
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
|
||||||
|
(eglot-luau-rojo-sourcemap-enabled . 't))))
|
||||||
6
.github/workflows/changelog.yml
vendored
6
.github/workflows/changelog.yml
vendored
@@ -11,12 +11,12 @@ jobs:
|
|||||||
name: Check Actions
|
name: Check Actions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Changelog check
|
- name: Changelog check
|
||||||
uses: Zomzog/changelog-checker@v1.3.0
|
uses: Zomzog/changelog-checker@v1.3.0
|
||||||
with:
|
with:
|
||||||
fileName: CHANGELOG.md
|
fileName: CHANGELOG.md
|
||||||
noChangelogLabel: skip changelog
|
noChangelogLabel: skip changelog
|
||||||
checkNotification: Simple
|
checkNotification: Simple
|
||||||
env:
|
env:
|
||||||
|
|||||||
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -26,13 +26,14 @@ jobs:
|
|||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Restore Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: actions/cache/restore@v4
|
||||||
|
|
||||||
- name: Setup Aftman
|
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
run: cargo build --locked --verbose
|
||||||
@@ -40,6 +41,15 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --locked --verbose
|
run: cargo test --locked --verbose
|
||||||
|
|
||||||
|
- name: Save Rust Cache
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
msrv:
|
msrv:
|
||||||
name: Check MSRV
|
name: Check MSRV
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -50,19 +60,29 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@1.70.0
|
uses: dtolnay/rust-toolchain@1.88.0
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Restore Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: actions/cache/restore@v4
|
||||||
|
|
||||||
- name: Setup Aftman
|
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
run: cargo build --locked --verbose
|
||||||
|
|
||||||
|
- name: Save Rust Cache
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Rustfmt, Clippy, Stylua, & Selene
|
name: Rustfmt, Clippy, Stylua, & Selene
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -77,13 +97,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Restore Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: actions/cache/restore@v4
|
||||||
|
|
||||||
- name: Setup Aftman
|
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Setup Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
with:
|
||||||
|
version: 'v1.1.0'
|
||||||
|
|
||||||
- name: Stylua
|
- name: Stylua
|
||||||
run: stylua --check plugin/src
|
run: stylua --check plugin/src
|
||||||
@@ -97,3 +123,11 @@ jobs:
|
|||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy
|
run: cargo clippy
|
||||||
|
|
||||||
|
- name: Save Rust Cache
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|||||||
51
.github/workflows/release.yml
vendored
51
.github/workflows/release.yml
vendored
@@ -25,15 +25,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Setup Rokit
|
||||||
uses: ok-nick/setup-aftman@v0.1.0
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
version: 'v1.1.0'
|
||||||
trust-check: false
|
|
||||||
version: 'v0.2.6'
|
|
||||||
|
|
||||||
- name: Build Plugin
|
- name: Build Plugin
|
||||||
run: rojo build plugin --output Rojo.rbxm
|
run: rojo build plugin.project.json --output Rojo.rbxm
|
||||||
|
|
||||||
- name: Upload Plugin to Release
|
- name: Upload Plugin to Release
|
||||||
env:
|
env:
|
||||||
@@ -55,15 +53,25 @@ jobs:
|
|||||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||||
include:
|
include:
|
||||||
- host: linux
|
- host: linux
|
||||||
os: ubuntu-20.04
|
os: ubuntu-22.04
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
label: linux-x86_64
|
label: linux-x86_64
|
||||||
|
|
||||||
|
- host: linux
|
||||||
|
os: ubuntu-22.04-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
label: linux-aarch64
|
||||||
|
|
||||||
- host: windows
|
- host: windows
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
label: windows-x86_64
|
label: windows-x86_64
|
||||||
|
|
||||||
|
- host: windows
|
||||||
|
os: windows-11-arm
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
label: windows-aarch64
|
||||||
|
|
||||||
- host: macos
|
- host: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
@@ -88,19 +96,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Restore Rust Cache
|
||||||
uses: ok-nick/setup-aftman@v0.1.0
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
path: |
|
||||||
trust-check: false
|
~/.cargo/registry
|
||||||
version: 'v0.2.6'
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||||
env:
|
|
||||||
# Build into a known directory so we can find our build artifact more
|
- name: Save Rust Cache
|
||||||
# easily.
|
uses: actions/cache/save@v4
|
||||||
CARGO_TARGET_DIR: output
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Generate Artifact Name
|
- name: Generate Artifact Name
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -117,11 +132,11 @@ jobs:
|
|||||||
mkdir staging
|
mkdir staging
|
||||||
|
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||||
cd staging
|
cd staging
|
||||||
7z a ../$ARTIFACT_NAME *
|
7z a ../$ARTIFACT_NAME *
|
||||||
else
|
else
|
||||||
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
||||||
cd staging
|
cd staging
|
||||||
zip ../$ARTIFACT_NAME *
|
zip ../$ARTIFACT_NAME *
|
||||||
fi
|
fi
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -10,8 +10,8 @@
|
|||||||
/*.rbxl
|
/*.rbxl
|
||||||
/*.rbxlx
|
/*.rbxlx
|
||||||
|
|
||||||
# Test places for the Roblox Studio Plugin
|
# Sourcemap for the Rojo plugin (for better intellisense)
|
||||||
/plugin/*.rbxlx
|
/sourcemap.json
|
||||||
|
|
||||||
# Roblox Studio holds 'lock' files on places
|
# Roblox Studio holds 'lock' files on places
|
||||||
*.rbxl.lock
|
*.rbxl.lock
|
||||||
@@ -19,3 +19,7 @@
|
|||||||
|
|
||||||
# Snapshot files from the 'insta' Rust crate
|
# Snapshot files from the 'insta' Rust crate
|
||||||
**/*.snap.new
|
**/*.snap.new
|
||||||
|
|
||||||
|
# Macos file system junk
|
||||||
|
._*
|
||||||
|
.DS_STORE
|
||||||
|
|||||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"JohnnyMorganz.luau-lsp",
|
||||||
|
"JohnnyMorganz.stylua",
|
||||||
|
"Kampfkarren.selene-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
|
||||||
|
"luau-lsp.sourcemap.autogenerate": true
|
||||||
|
}
|
||||||
1056
CHANGELOG.md
1056
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,29 @@ You'll want these tools to work on Rojo:
|
|||||||
|
|
||||||
* Latest stable Rust compiler
|
* Latest stable Rust compiler
|
||||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||||
* [Foreman](https://github.com/Roblox/foreman)
|
* [Rokit](https://github.com/rojo-rbx/rokit)
|
||||||
|
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
||||||
|
|
||||||
|
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
|
||||||
|
|
||||||
|
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/watch-build-plugin.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also run the plugin's unit tests with the following:
|
||||||
|
|
||||||
|
*(Make sure you have `run-in-roblox` installed first!)*
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/unit-test-plugin.sh
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
Documentation impacts way more people than the individual lines of code we write.
|
Documentation impacts way more people than the individual lines of code we write.
|
||||||
|
|
||||||
If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||||
|
|
||||||
## Bug Reports and Feature Requests
|
## Bug Reports and Feature Requests
|
||||||
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
||||||
|
|||||||
1878
Cargo.lock
generated
1878
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
40
Cargo.toml
@@ -1,8 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.4.0"
|
version = "7.7.0-rc.1"
|
||||||
rust-version = "1.70.0"
|
rust-version = "1.88"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = [
|
||||||
|
"Lucien Greathouse <me@lpghatguy.com>",
|
||||||
|
"Micah Reid <git@dekkonot.com>",
|
||||||
|
"Ken Loeffler <kenloef@gmail.com>",
|
||||||
|
]
|
||||||
description = "Enables professional-grade development tools for Roblox developers"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
homepage = "https://rojo.space"
|
homepage = "https://rojo.space"
|
||||||
@@ -42,20 +46,22 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
memofs = { version = "0.3.1", path = "crates/memofs" }
|
||||||
|
|
||||||
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
||||||
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [
|
||||||
|
# "unstable_text_format",
|
||||||
|
# ] }
|
||||||
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
||||||
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
||||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||||
|
|
||||||
rbx_binary = "0.7.7"
|
rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] }
|
||||||
rbx_dom_weak = "2.9.0"
|
rbx_dom_weak = "4.1.0"
|
||||||
rbx_reflection = "4.7.0"
|
rbx_reflection = "6.1.0"
|
||||||
rbx_reflection_database = "0.2.12"
|
rbx_reflection_database = "2.0.2"
|
||||||
rbx_xml = "0.13.5"
|
rbx_xml = "2.0.1"
|
||||||
|
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.80"
|
||||||
backtrace = "0.3.69"
|
backtrace = "0.3.69"
|
||||||
@@ -68,9 +74,9 @@ futures = "0.3.30"
|
|||||||
globset = "0.4.14"
|
globset = "0.4.14"
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||||
|
hyper-tungstenite = "0.11.0"
|
||||||
jod-thread = "0.1.2"
|
jod-thread = "0.1.2"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
maplit = "1.0.2"
|
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
opener = "0.5.2"
|
opener = "0.5.2"
|
||||||
rayon = "1.9.0"
|
rayon = "1.9.0"
|
||||||
@@ -82,14 +88,22 @@ reqwest = { version = "0.11.24", default-features = false, features = [
|
|||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
roblox_install = "1.0.0"
|
roblox_install = "1.0.0"
|
||||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.114"
|
serde_json = "1.0.145"
|
||||||
|
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
||||||
|
strum = { version = "0.27", features = ["derive"] }
|
||||||
toml = "0.5.11"
|
toml = "0.5.11"
|
||||||
termcolor = "1.4.1"
|
termcolor = "1.4.1"
|
||||||
thiserror = "1.0.57"
|
thiserror = "1.0.57"
|
||||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||||
clap = { version = "3.2.25", features = ["derive"] }
|
clap = { version = "3.2.25", features = ["derive"] }
|
||||||
profiling = "1.0.15"
|
profiling = "1.0.15"
|
||||||
|
yaml-rust2 = "0.10.3"
|
||||||
|
data-encoding = "2.8.0"
|
||||||
|
|
||||||
|
blake3 = "1.5.0"
|
||||||
|
float-cmp = "0.9.0"
|
||||||
|
indexmap = { version = "2.10.0", features = ["serde"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
|||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
Rojo supports Rust 1.70.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.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
[tools]
|
|
||||||
rojo = "rojo-rbx/rojo@7.3.0"
|
|
||||||
selene = "Kampfkarren/selene@0.26.1"
|
|
||||||
stylua = "JohnnyMorganz/stylua@0.18.2"
|
|
||||||
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
|
||||||
@@ -17,6 +17,10 @@ html {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #e7e7e7
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width:100%;
|
max-width:100%;
|
||||||
max-height:100%;
|
max-height:100%;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# Roblox Studio lock files
|
# Roblox Studio lock files
|
||||||
/*.rbxlx.lock
|
/*.rbxlx.lock
|
||||||
/*.rbxl.lock
|
/*.rbxl.lock
|
||||||
|
|
||||||
|
sourcemap.json
|
||||||
@@ -2,4 +2,4 @@ return {
|
|||||||
hello = function()
|
hello = function()
|
||||||
print("Hello world, from {project_name}!")
|
print("Hello world, from {project_name}!")
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,6 @@
|
|||||||
|
|
||||||
# Roblox Studio lock files
|
# Roblox Studio lock files
|
||||||
/*.rbxlx.lock
|
/*.rbxlx.lock
|
||||||
/*.rbxl.lock
|
/*.rbxl.lock
|
||||||
|
|
||||||
|
sourcemap.json
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("Hello world, from client!")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("Hello world, from server!")
|
||||||
3
assets/project-templates/place/src/shared/Hello.luau
Normal file
3
assets/project-templates/place/src/shared/Hello.luau
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
return function()
|
||||||
|
print("Hello, world!")
|
||||||
|
end
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
# Plugin model files
|
# Plugin model files
|
||||||
/{project_name}.rbxmx
|
/{project_name}.rbxmx
|
||||||
/{project_name}.rbxm
|
/{project_name}.rbxm
|
||||||
|
|
||||||
|
sourcemap.json
|
||||||
1
assets/project-templates/plugin/src/init.server.luau
Normal file
1
assets/project-templates/plugin/src/init.server.luau
Normal file
@@ -0,0 +1 @@
|
|||||||
|
print("Hello world, from plugin!")
|
||||||
40
build.rs
40
build.rs
@@ -20,6 +20,10 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
|||||||
|
|
||||||
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
if file_name.starts_with(".git") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// We can skip any TestEZ test files since they aren't necessary for
|
// We can skip any TestEZ test files since they aren't necessary for
|
||||||
// the plugin to run.
|
// the plugin to run.
|
||||||
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
||||||
@@ -41,33 +45,39 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
|||||||
fn main() -> Result<(), anyhow::Error> {
|
fn main() -> Result<(), anyhow::Error> {
|
||||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
|
||||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
let plugin_dir = root_dir.join("plugin");
|
||||||
|
let templates_dir = root_dir.join("assets").join("project-templates");
|
||||||
|
|
||||||
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||||
let plugin_version =
|
let plugin_version =
|
||||||
Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
|
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
our_version, plugin_version,
|
our_version, plugin_version,
|
||||||
"plugin version does not match Cargo version"
|
"plugin version does not match Cargo version"
|
||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
let template_snapshot = snapshot_from_fs_path(&templates_dir)?;
|
||||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
|
||||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
let plugin_snapshot = VfsSnapshot::dir(hashmap! {
|
||||||
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
||||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
"plugin" => VfsSnapshot::dir(hashmap! {
|
||||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||||
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||||
"Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||||
|
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||||
|
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||||
|
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?;
|
||||||
let out_file = File::create(out_path)?;
|
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?;
|
||||||
|
|
||||||
bincode::serialize_into(out_file, &snapshot)?;
|
bincode::serialize_into(plugin_file, &plugin_snapshot)?;
|
||||||
|
bincode::serialize_into(template_file, &template_snapshot)?;
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
|
||||||
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
println!("cargo:rerun-if-changed=build/windows/rojo.manifest");
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## 0.3.1 (2025-11-27)
|
||||||
|
* Added `Vfs::exists`. [#1169]
|
||||||
|
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
|
||||||
|
|
||||||
|
[#1169]: https://github.com/rojo-rbx/rojo/pull/1169
|
||||||
|
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
||||||
|
|
||||||
## 0.3.0 (2024-03-15)
|
## 0.3.0 (2024-03-15)
|
||||||
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
||||||
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = [
|
||||||
|
"Lucien Greathouse <me@lpghatguy.com>",
|
||||||
|
"Micah Reid <git@dekkonot.com>",
|
||||||
|
"Ken Loeffler <kenloef@gmail.com>",
|
||||||
|
]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ impl VfsBackend for InMemoryFs {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
||||||
|
let inner = self.inner.lock().unwrap();
|
||||||
|
Ok(inner.entries.contains_key(path))
|
||||||
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
@@ -176,6 +181,21 @@ impl VfsBackend for InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
let mut path_buf = path.to_path_buf();
|
||||||
|
while let Some(parent) = path_buf.parent() {
|
||||||
|
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
|
||||||
|
path_buf.pop();
|
||||||
|
}
|
||||||
|
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
let mut inner = self.inner.lock().unwrap();
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
@@ -228,23 +248,17 @@ impl VfsBackend for InMemoryFs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
fn must_be_file<T>(path: &Path) -> io::Result<T> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other(format!(
|
||||||
io::ErrorKind::Other,
|
"path {} was a directory, but must be a file",
|
||||||
format!(
|
path.display()
|
||||||
"path {} was a directory, but must be a file",
|
)))
|
||||||
path.display()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
fn must_be_dir<T>(path: &Path) -> io::Result<T> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other(format!(
|
||||||
io::ErrorKind::Other,
|
"path {} was a file, but must be a directory",
|
||||||
format!(
|
path.display()
|
||||||
"path {} was a file, but must be a directory",
|
)))
|
||||||
path.display()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn not_found<T>(path: &Path) -> io::Result<T> {
|
fn not_found<T>(path: &Path) -> io::Result<T> {
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
|
|||||||
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
||||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
||||||
|
fn exists(&mut self, path: &Path) -> io::Result<bool>;
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
||||||
|
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
|
||||||
|
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||||
@@ -173,6 +176,11 @@ impl VfsInner {
|
|||||||
Ok(Arc::new(contents_str.into()))
|
Ok(Arc::new(contents_str.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.backend.exists(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let contents = contents.as_ref();
|
let contents = contents.as_ref();
|
||||||
@@ -190,6 +198,16 @@ impl VfsInner {
|
|||||||
Ok(dir)
|
Ok(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.backend.create_dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.backend.create_dir_all(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let _ = self.backend.unwatch(path);
|
let _ = self.backend.unwatch(path);
|
||||||
@@ -326,6 +344,42 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().read_dir(path)
|
self.inner.lock().unwrap().read_dir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return whether the given path exists.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
|
||||||
|
///
|
||||||
|
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
|
||||||
|
#[inline]
|
||||||
|
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.lock().unwrap().exists(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a directory at the provided location.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
||||||
|
/// Similiar to that function, this function will fail if the parent of the
|
||||||
|
/// path does not exist.
|
||||||
|
///
|
||||||
|
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
||||||
|
#[inline]
|
||||||
|
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.lock().unwrap().create_dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a directory at the provided location, recursively creating
|
||||||
|
/// all parent components if they are missing.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
||||||
|
///
|
||||||
|
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
||||||
|
#[inline]
|
||||||
|
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.lock().unwrap().create_dir_all(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a file.
|
/// Remove a file.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||||
@@ -428,6 +482,31 @@ impl VfsLock<'_> {
|
|||||||
self.inner.read_dir(path)
|
self.inner.read_dir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a directory at the provided location.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
||||||
|
/// Similiar to that function, this function will fail if the parent of the
|
||||||
|
/// path does not exist.
|
||||||
|
///
|
||||||
|
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
||||||
|
#[inline]
|
||||||
|
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.create_dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a directory at the provided location, recursively creating
|
||||||
|
/// all parent components if they are missing.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
||||||
|
///
|
||||||
|
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
||||||
|
#[inline]
|
||||||
|
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.create_dir_all(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a file.
|
/// Remove a file.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||||
|
|||||||
@@ -15,45 +15,39 @@ impl NoopBackend {
|
|||||||
|
|
||||||
impl VfsBackend for NoopBackend {
|
impl VfsBackend for NoopBackend {
|
||||||
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
}
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
fn exists(&mut self, _path: &Path) -> io::Result<bool> {
|
||||||
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
}
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
@@ -61,17 +55,11 @@ impl VfsBackend for NoopBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
fn watch(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
|
||||||
Err(io::Error::new(
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
io::ErrorKind::Other,
|
|
||||||
"NoopBackend doesn't do anything",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ impl VfsBackend for StdBackend {
|
|||||||
fs_err::write(path, data)
|
fs_err::write(path, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
||||||
|
std::fs::exists(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||||
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||||
let mut entries = entries?;
|
let mut entries = entries?;
|
||||||
@@ -78,6 +82,14 @@ impl VfsBackend for StdBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
fs_err::create_dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
fs_err::create_dir_all(path)
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
fs_err::remove_file(path)
|
fs_err::remove_file(path)
|
||||||
}
|
}
|
||||||
@@ -109,15 +121,13 @@ impl VfsBackend for StdBackend {
|
|||||||
self.watches.insert(path.to_path_buf());
|
self.watches.insert(path.to_path_buf());
|
||||||
self.watcher
|
self.watcher
|
||||||
.watch(path, RecursiveMode::Recursive)
|
.watch(path, RecursiveMode::Recursive)
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
.map_err(io::Error::other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
self.watches.remove(path);
|
self.watches.remove(path);
|
||||||
self.watcher
|
self.watcher.unwatch(path).map_err(io::Error::other)
|
||||||
.unwatch(path)
|
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,13 @@ use serde::Serialize;
|
|||||||
/// Enables redacting any value that serializes as a string.
|
/// Enables redacting any value that serializes as a string.
|
||||||
///
|
///
|
||||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct RedactionMap {
|
pub struct RedactionMap {
|
||||||
ids: HashMap<String, usize>,
|
ids: HashMap<String, usize>,
|
||||||
last_id: usize,
|
last_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedactionMap {
|
impl RedactionMap {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
ids: HashMap::new(),
|
|
||||||
last_id: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||||
let id = id.to_string();
|
let id = id.to_string();
|
||||||
|
|
||||||
@@ -28,6 +22,12 @@ impl RedactionMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the numeric ID that was assigned to the provided value,
|
||||||
|
/// if one exists.
|
||||||
|
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
|
||||||
|
self.ids.get(&value.to_string()).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn intern(&mut self, id: impl ToString) {
|
pub fn intern(&mut self, id: impl ToString) {
|
||||||
let last_id = &mut self.last_id;
|
let last_id = &mut self.last_id;
|
||||||
|
|
||||||
|
|||||||
@@ -3,25 +3,25 @@
|
|||||||
"tree": {
|
"tree": {
|
||||||
"$className": "Folder",
|
"$className": "Folder",
|
||||||
"Plugin": {
|
"Plugin": {
|
||||||
"$path": "src"
|
"$path": "plugin/src"
|
||||||
},
|
},
|
||||||
"Packages": {
|
"Packages": {
|
||||||
"$path": "Packages",
|
"$path": "plugin/Packages",
|
||||||
"Log": {
|
"Log": {
|
||||||
"$path": "log"
|
"$path": "plugin/log"
|
||||||
},
|
},
|
||||||
"Http": {
|
"Http": {
|
||||||
"$path": "http"
|
"$path": "plugin/http"
|
||||||
},
|
},
|
||||||
"Fmt": {
|
"Fmt": {
|
||||||
"$path": "fmt"
|
"$path": "plugin/fmt"
|
||||||
},
|
},
|
||||||
"RbxDom": {
|
"RbxDom": {
|
||||||
"$path": "rbx_dom_lua"
|
"$path": "plugin/rbx_dom_lua"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Version": {
|
"Version": {
|
||||||
"$path": "Version.txt"
|
"$path": "plugin/Version.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Submodule plugin/Packages/Highlighter updated: e0d061449e...c12c488dad
Submodule plugin/Packages/t updated: 1f9754254b...1dbfccc182
@@ -1 +1 @@
|
|||||||
7.4.0
|
7.7.0-rc.1
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
local function defaultTableDebug(buffer, input)
|
local function defaultTableDebug(buffer, input)
|
||||||
buffer:writeRaw("{")
|
buffer:writeRaw("{")
|
||||||
|
|
||||||
for key, value in pairs(input) do
|
for key, value in input do
|
||||||
buffer:write("[{:?}] = {:?}", key, value)
|
buffer:write("[{:?}] = {:?}", key, value)
|
||||||
|
|
||||||
if next(input, key) ~= nil then
|
if next(input, key) ~= nil then
|
||||||
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
|
|||||||
buffer:writeLineRaw("{")
|
buffer:writeLineRaw("{")
|
||||||
buffer:indent()
|
buffer:indent()
|
||||||
|
|
||||||
for key, value in pairs(input) do
|
for key, value in input do
|
||||||
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
|
|||||||
elseif valueType == "table" then
|
elseif valueType == "table" then
|
||||||
local valueMeta = getmetatable(value)
|
local valueMeta = getmetatable(value)
|
||||||
|
|
||||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||||
-- This type implement's the metamethod we made up to line up with
|
-- This type implement's the metamethod we made up to line up with
|
||||||
-- Rust's 'Debug' trait.
|
-- Rust's 'Debug' trait.
|
||||||
|
|
||||||
@@ -242,4 +242,4 @@ return {
|
|||||||
debugOutputBuffer = debugOutputBuffer,
|
debugOutputBuffer = debugOutputBuffer,
|
||||||
fmt = fmt,
|
fmt = fmt,
|
||||||
debugify = debugify,
|
debugify = debugify,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ function Response:json()
|
|||||||
return HttpService:JSONDecode(self.body)
|
return HttpService:JSONDecode(self.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Response
|
return Response
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ return function()
|
|||||||
it("should load", function()
|
it("should load", function()
|
||||||
require(script.Parent)
|
require(script.Parent)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -57,4 +57,4 @@ function Log.error(template, ...)
|
|||||||
error(Fmt.fmt(template, ...))
|
error(Fmt.fmt(template, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
return Log
|
return Log
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ return function()
|
|||||||
it("should load", function()
|
it("should load", function()
|
||||||
require(script.Parent)
|
require(script.Parent)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -188,6 +188,38 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Content = {
|
Content = {
|
||||||
|
fromPod = function(pod): Content
|
||||||
|
if type(pod) == "string" then
|
||||||
|
if pod == "None" then
|
||||||
|
return Content.none
|
||||||
|
else
|
||||||
|
error(`unexpected Content value '{pod}'`)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local ty, value = next(pod)
|
||||||
|
if ty == "Uri" then
|
||||||
|
return Content.fromUri(value)
|
||||||
|
elseif ty == "Object" then
|
||||||
|
error("Object deserializing is not currently implemented")
|
||||||
|
else
|
||||||
|
error(`Unknown Content type '{ty}' (could not deserialize)`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
toPod = function(roblox: Content)
|
||||||
|
if roblox.SourceType == Enum.ContentSourceType.None then
|
||||||
|
return "None"
|
||||||
|
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
|
||||||
|
return { Uri = roblox.Uri }
|
||||||
|
elseif roblox.SourceType == Enum.ContentSourceType.Object then
|
||||||
|
error("Object serializing is not currently implemented")
|
||||||
|
else
|
||||||
|
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
|
ContentId = {
|
||||||
fromPod = identity,
|
fromPod = identity,
|
||||||
toPod = identity,
|
toPod = identity,
|
||||||
},
|
},
|
||||||
@@ -205,6 +237,19 @@ types = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
EnumItem = {
|
||||||
|
fromPod = function(pod)
|
||||||
|
return Enum[pod.type]:FromValue(pod.value)
|
||||||
|
end,
|
||||||
|
|
||||||
|
toPod = function(roblox)
|
||||||
|
return {
|
||||||
|
type = tostring(roblox.EnumType),
|
||||||
|
value = roblox.Value,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
Faces = {
|
Faces = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
local faces = {}
|
local faces = {}
|
||||||
@@ -300,7 +345,12 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
for index, keypoint in ipairs(pod.keypoints) do
|
||||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
|
-- TODO: Add a test for NaN or Infinity values and envelopes
|
||||||
|
-- Right now it isn't possible because it'd fail the roundtrip.
|
||||||
|
-- It's more important that it works right now, though.
|
||||||
|
local value = keypoint.value or 0
|
||||||
|
local envelope = keypoint.envelope or 0
|
||||||
|
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
|
||||||
end
|
end
|
||||||
|
|
||||||
return NumberSequence.new(keypoints)
|
return NumberSequence.new(keypoints)
|
||||||
@@ -328,13 +378,26 @@ types = {
|
|||||||
if pod == "Default" then
|
if pod == "Default" then
|
||||||
return nil
|
return nil
|
||||||
else
|
else
|
||||||
return PhysicalProperties.new(
|
-- Passing `nil` instead of not passing anything gives
|
||||||
pod.density,
|
-- different results, so we have to branch here.
|
||||||
pod.friction,
|
if pod.acousticAbsorption then
|
||||||
pod.elasticity,
|
return (PhysicalProperties.new :: any)(
|
||||||
pod.frictionWeight,
|
pod.density,
|
||||||
pod.elasticityWeight
|
pod.friction,
|
||||||
)
|
pod.elasticity,
|
||||||
|
pod.frictionWeight,
|
||||||
|
pod.elasticityWeight,
|
||||||
|
pod.acousticAbsorption
|
||||||
|
)
|
||||||
|
else
|
||||||
|
return PhysicalProperties.new(
|
||||||
|
pod.density,
|
||||||
|
pod.friction,
|
||||||
|
pod.elasticity,
|
||||||
|
pod.frictionWeight,
|
||||||
|
pod.elasticityWeight
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
@@ -348,6 +411,7 @@ types = {
|
|||||||
elasticity = roblox.Elasticity,
|
elasticity = roblox.Elasticity,
|
||||||
frictionWeight = roblox.FrictionWeight,
|
frictionWeight = roblox.FrictionWeight,
|
||||||
elasticityWeight = roblox.ElasticityWeight,
|
elasticityWeight = roblox.ElasticityWeight,
|
||||||
|
acousticAbsorption = roblox.AcousticAbsorption,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Error.Kind = {
|
|||||||
UnknownProperty = "UnknownProperty",
|
UnknownProperty = "UnknownProperty",
|
||||||
PropertyNotReadable = "PropertyNotReadable",
|
PropertyNotReadable = "PropertyNotReadable",
|
||||||
PropertyNotWritable = "PropertyNotWritable",
|
PropertyNotWritable = "PropertyNotWritable",
|
||||||
|
CannotParseBinaryString = "CannotParseBinaryString",
|
||||||
Roblox = "Roblox",
|
Roblox = "Roblox",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
0.0
|
0.0
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"TestEnumItem": {
|
||||||
|
"EnumItem": {
|
||||||
|
"type": "Material",
|
||||||
|
"value": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
"TestNumber": {
|
"TestNumber": {
|
||||||
"Float64": 1337.0
|
"Float64": 1337.0
|
||||||
},
|
},
|
||||||
@@ -170,9 +176,23 @@
|
|||||||
},
|
},
|
||||||
"ty": "ColorSequence"
|
"ty": "ColorSequence"
|
||||||
},
|
},
|
||||||
"Content": {
|
"ContentId": {
|
||||||
"value": {
|
"value": {
|
||||||
"Content": "rbxassetid://12345"
|
"ContentId": "rbxassetid://12345"
|
||||||
|
},
|
||||||
|
"ty": "ContentId"
|
||||||
|
},
|
||||||
|
"Content_None": {
|
||||||
|
"value": {
|
||||||
|
"Content": "None"
|
||||||
|
},
|
||||||
|
"ty": "Content"
|
||||||
|
},
|
||||||
|
"Content_Uri": {
|
||||||
|
"value": {
|
||||||
|
"Content": {
|
||||||
|
"Uri": "rbxasset://abc/123.rojo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ty": "Content"
|
"ty": "Content"
|
||||||
},
|
},
|
||||||
@@ -182,6 +202,15 @@
|
|||||||
},
|
},
|
||||||
"ty": "Enum"
|
"ty": "Enum"
|
||||||
},
|
},
|
||||||
|
"EnumItem": {
|
||||||
|
"value": {
|
||||||
|
"EnumItem": {
|
||||||
|
"type": "Material",
|
||||||
|
"value": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "EnumItem"
|
||||||
|
},
|
||||||
"Faces": {
|
"Faces": {
|
||||||
"value": {
|
"value": {
|
||||||
"Faces": [
|
"Faces": [
|
||||||
@@ -412,7 +441,8 @@
|
|||||||
"friction": 1.0,
|
"friction": 1.0,
|
||||||
"elasticity": 0.0,
|
"elasticity": 0.0,
|
||||||
"frictionWeight": 50.0,
|
"frictionWeight": 50.0,
|
||||||
"elasticityWeight": 25.0
|
"elasticityWeight": 25.0,
|
||||||
|
"acousticAbsorption": 0.15625
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ty": "PhysicalProperties"
|
"ty": "PhysicalProperties"
|
||||||
|
|||||||
@@ -1,139 +1,10 @@
|
|||||||
-- Thanks to Tiffany352 for this base64 implementation!
|
local EncodingService = game:GetService("EncodingService")
|
||||||
|
|
||||||
local floor = math.floor
|
|
||||||
local char = string.char
|
|
||||||
|
|
||||||
local function encodeBase64(str)
|
|
||||||
local out = {}
|
|
||||||
local nOut = 0
|
|
||||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
||||||
local strLen = #str
|
|
||||||
|
|
||||||
-- 3 octets become 4 hextets
|
|
||||||
for i = 1, strLen - 2, 3 do
|
|
||||||
local b1, b2, b3 = str:byte(i, i + 3)
|
|
||||||
local word = b3 + b2 * 256 + b1 * 256 * 256
|
|
||||||
|
|
||||||
local h4 = word % 64 + 1
|
|
||||||
word = floor(word / 64)
|
|
||||||
local h3 = word % 64 + 1
|
|
||||||
word = floor(word / 64)
|
|
||||||
local h2 = word % 64 + 1
|
|
||||||
word = floor(word / 64)
|
|
||||||
local h1 = word % 64 + 1
|
|
||||||
|
|
||||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
|
||||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
|
||||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
|
||||||
out[nOut + 4] = alphabet:sub(h4, h4)
|
|
||||||
nOut = nOut + 4
|
|
||||||
end
|
|
||||||
|
|
||||||
local remainder = strLen % 3
|
|
||||||
|
|
||||||
if remainder == 2 then
|
|
||||||
-- 16 input bits -> 3 hextets (2 full, 1 partial)
|
|
||||||
local b1, b2 = str:byte(-2, -1)
|
|
||||||
-- partial is 4 bits long, leaving 2 bits of zero padding ->
|
|
||||||
-- offset = 4
|
|
||||||
local word = b2 * 4 + b1 * 4 * 256
|
|
||||||
|
|
||||||
local h3 = word % 64 + 1
|
|
||||||
word = floor(word / 64)
|
|
||||||
local h2 = word % 64 + 1
|
|
||||||
word = floor(word / 64)
|
|
||||||
local h1 = word % 64 + 1
|
|
||||||
|
|
||||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
|
||||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
|
||||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
|
||||||
out[nOut + 4] = "="
|
|
||||||
elseif remainder == 1 then
|
|
||||||
-- 8 input bits -> 2 hextets (2 full, 1 partial)
|
|
||||||
local b1 = str:byte(-1, -1)
|
|
||||||
-- partial is 2 bits long, leaving 4 bits of zero padding ->
|
|
||||||
-- offset = 16
|
|
||||||
local word = b1 * 16
|
|
||||||
|
|
||||||
local h2 = word % 64 + 1
|
|
||||||
word = floor(word / 64)
|
|
||||||
local h1 = word % 64 + 1
|
|
||||||
|
|
||||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
|
||||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
|
||||||
out[nOut + 3] = "="
|
|
||||||
out[nOut + 4] = "="
|
|
||||||
end
|
|
||||||
-- if the remainder is 0, then no work is needed
|
|
||||||
|
|
||||||
return table.concat(out, "")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function decodeBase64(str)
|
|
||||||
local out = {}
|
|
||||||
local nOut = 0
|
|
||||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
||||||
local strLen = #str
|
|
||||||
local acc = 0
|
|
||||||
local nAcc = 0
|
|
||||||
|
|
||||||
local alphabetLut = {}
|
|
||||||
for i = 1, #alphabet do
|
|
||||||
alphabetLut[alphabet:sub(i, i)] = i - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 4 hextets become 3 octets
|
|
||||||
for i = 1, strLen do
|
|
||||||
local ch = str:sub(i, i)
|
|
||||||
local byte = alphabetLut[ch]
|
|
||||||
if byte then
|
|
||||||
acc = acc * 64 + byte
|
|
||||||
nAcc = nAcc + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if nAcc == 4 then
|
|
||||||
local b3 = acc % 256
|
|
||||||
acc = floor(acc / 256)
|
|
||||||
local b2 = acc % 256
|
|
||||||
acc = floor(acc / 256)
|
|
||||||
local b1 = acc % 256
|
|
||||||
|
|
||||||
out[nOut + 1] = char(b1)
|
|
||||||
out[nOut + 2] = char(b2)
|
|
||||||
out[nOut + 3] = char(b3)
|
|
||||||
nOut = nOut + 3
|
|
||||||
nAcc = 0
|
|
||||||
acc = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if nAcc == 3 then
|
|
||||||
-- 3 hextets -> 16 bit output
|
|
||||||
acc = acc * 64
|
|
||||||
acc = floor(acc / 256)
|
|
||||||
local b2 = acc % 256
|
|
||||||
acc = floor(acc / 256)
|
|
||||||
local b1 = acc % 256
|
|
||||||
|
|
||||||
out[nOut + 1] = char(b1)
|
|
||||||
out[nOut + 2] = char(b2)
|
|
||||||
elseif nAcc == 2 then
|
|
||||||
-- 2 hextets -> 8 bit output
|
|
||||||
acc = acc * 64
|
|
||||||
acc = floor(acc / 256)
|
|
||||||
acc = acc * 64
|
|
||||||
acc = floor(acc / 256)
|
|
||||||
local b1 = acc % 256
|
|
||||||
|
|
||||||
out[nOut + 1] = char(b1)
|
|
||||||
elseif nAcc == 1 then
|
|
||||||
error("Base64 has invalid length")
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(out, "")
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
decode = decodeBase64,
|
decode = function(input: string)
|
||||||
encode = encodeBase64,
|
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
|
||||||
|
end,
|
||||||
|
encode = function(input: string)
|
||||||
|
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
|
||||||
|
end,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
local CollectionService = game:GetService("CollectionService")
|
local CollectionService = game:GetService("CollectionService")
|
||||||
local ScriptEditorService = game:GetService("ScriptEditorService")
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||||
|
|
||||||
|
local Error = require(script.Parent.Error)
|
||||||
|
|
||||||
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||||
local TERRAIN_MATERIAL_COLORS = {
|
local TERRAIN_MATERIAL_COLORS = {
|
||||||
Enum.Material.Grass,
|
Enum.Material.Grass,
|
||||||
@@ -51,6 +53,10 @@ return {
|
|||||||
return true, instance:GetAttributes()
|
return true, instance:GetAttributes()
|
||||||
end,
|
end,
|
||||||
write = function(instance, _, value)
|
write = function(instance, _, value)
|
||||||
|
if typeof(value) ~= "table" then
|
||||||
|
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||||
|
end
|
||||||
|
|
||||||
local existing = instance:GetAttributes()
|
local existing = instance:GetAttributes()
|
||||||
local didAllWritesSucceed = true
|
local didAllWritesSucceed = true
|
||||||
|
|
||||||
@@ -160,9 +166,14 @@ return {
|
|||||||
return true, colors
|
return true, colors
|
||||||
end,
|
end,
|
||||||
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||||
|
if typeof(value) ~= "table" then
|
||||||
|
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||||
|
end
|
||||||
|
|
||||||
for material, color in value do
|
for material, color in value do
|
||||||
instance:SetMaterialColor(material, color)
|
instance:SetMaterialColor(material, color)
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -197,4 +208,30 @@ return {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
StyleRule = {
|
||||||
|
PropertiesSerialize = {
|
||||||
|
read = function(instance: StyleRule)
|
||||||
|
return true, instance:GetProperties()
|
||||||
|
end,
|
||||||
|
write = function(instance: StyleRule, _, value: { [any]: any })
|
||||||
|
if typeof(value) ~= "table" then
|
||||||
|
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||||
|
end
|
||||||
|
|
||||||
|
local existing = instance:GetProperties()
|
||||||
|
|
||||||
|
for itemName, itemValue in pairs(value) do
|
||||||
|
instance:SetProperty(itemName, itemValue)
|
||||||
|
end
|
||||||
|
|
||||||
|
for existingItemName in pairs(existing) do
|
||||||
|
if value[existingItemName] == nil then
|
||||||
|
instance:SetProperty(existingItemName, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
|
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
|
||||||
|
|
||||||
local Rojo = ReplicatedStorage.Rojo
|
local Rojo = ReplicatedStorage.Rojo
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local Packages = script.Parent.Parent.Packages
|
local Packages = script.Parent.Parent.Packages
|
||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
local Http = require(Packages.Http)
|
local Http = require(Packages.Http)
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
local Promise = require(Packages.Promise)
|
local Promise = require(Packages.Promise)
|
||||||
@@ -9,7 +10,9 @@ local Version = require(script.Parent.Version)
|
|||||||
|
|
||||||
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
local validateApiSocketPacket = Types.ifEnabled(Types.ApiSocketPacket)
|
||||||
|
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
|
||||||
|
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
|
||||||
|
|
||||||
local function rejectFailedRequests(response)
|
local function rejectFailedRequests(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
@@ -45,14 +48,7 @@ end
|
|||||||
|
|
||||||
local function rejectWrongPlaceId(infoResponseBody)
|
local function rejectWrongPlaceId(infoResponseBody)
|
||||||
if infoResponseBody.expectedPlaceIds ~= nil then
|
if infoResponseBody.expectedPlaceIds ~= nil then
|
||||||
local foundId = false
|
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
|
||||||
|
|
||||||
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
|
|
||||||
if id == game.PlaceId then
|
|
||||||
foundId = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not foundId then
|
if not foundId then
|
||||||
local idList = {}
|
local idList = {}
|
||||||
@@ -62,10 +58,30 @@ local function rejectWrongPlaceId(infoResponseBody)
|
|||||||
|
|
||||||
local message = (
|
local message = (
|
||||||
"Found a Rojo server, but its project is set to only be used with a specific list of places."
|
"Found a Rojo server, but its project is set to only be used with a specific list of places."
|
||||||
.. "\nYour place ID is %s, but needs to be one of these:"
|
.. "\nYour place ID is %u, but needs to be one of these:"
|
||||||
.. "\n%s"
|
.. "\n%s"
|
||||||
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
||||||
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
|
):format(game.PlaceId, table.concat(idList, "\n"))
|
||||||
|
|
||||||
|
return Promise.reject(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if infoResponseBody.unexpectedPlaceIds ~= nil then
|
||||||
|
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
|
||||||
|
|
||||||
|
if foundId then
|
||||||
|
local idList = {}
|
||||||
|
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
|
||||||
|
table.insert(idList, "- " .. tostring(id))
|
||||||
|
end
|
||||||
|
|
||||||
|
local message = (
|
||||||
|
"Found a Rojo server, but its project is set to not be used with a specific list of places."
|
||||||
|
.. "\nYour place ID is %u, but needs to not be one of these:"
|
||||||
|
.. "\n%s"
|
||||||
|
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
|
||||||
|
):format(game.PlaceId, table.concat(idList, "\n"))
|
||||||
|
|
||||||
return Promise.reject(message)
|
return Promise.reject(message)
|
||||||
end
|
end
|
||||||
@@ -84,6 +100,7 @@ function ApiContext.new(baseUrl)
|
|||||||
__baseUrl = baseUrl,
|
__baseUrl = baseUrl,
|
||||||
__sessionId = nil,
|
__sessionId = nil,
|
||||||
__messageCursor = -1,
|
__messageCursor = -1,
|
||||||
|
__wsClient = nil,
|
||||||
__connected = true,
|
__connected = true,
|
||||||
__activeRequests = {},
|
__activeRequests = {},
|
||||||
}
|
}
|
||||||
@@ -111,6 +128,12 @@ function ApiContext:disconnect()
|
|||||||
request:cancel()
|
request:cancel()
|
||||||
end
|
end
|
||||||
self.__activeRequests = {}
|
self.__activeRequests = {}
|
||||||
|
|
||||||
|
if self.__wsClient then
|
||||||
|
Log.trace("Closing WebSocket client")
|
||||||
|
self.__wsClient:Close()
|
||||||
|
end
|
||||||
|
self.__wsClient = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:setMessageCursor(index)
|
function ApiContext:setMessageCursor(index)
|
||||||
@@ -192,38 +215,65 @@ function ApiContext:write(patch)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:retrieveMessages()
|
function ApiContext:connectWebSocket(packetHandlers)
|
||||||
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
local url = ("%s/api/socket/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||||
|
-- Convert HTTP/HTTPS URL to WS/WSS
|
||||||
|
url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://")
|
||||||
|
|
||||||
local function sendRequest()
|
return Promise.new(function(resolve, reject)
|
||||||
local request = Http.get(url):catch(function(err)
|
local success, wsClient =
|
||||||
if err.type == Http.Error.Kind.Timeout and self.__connected then
|
pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, {
|
||||||
return sendRequest()
|
Url = url,
|
||||||
|
})
|
||||||
|
if not success then
|
||||||
|
reject("Failed to create WebSocket client: " .. tostring(wsClient))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.__wsClient = wsClient
|
||||||
|
|
||||||
|
local closed, errored, received
|
||||||
|
|
||||||
|
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
||||||
|
local data = Http.jsonDecode(msg)
|
||||||
|
if data.sessionId ~= self.__sessionId then
|
||||||
|
Log.warn("Received message with wrong session ID; ignoring")
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise.reject(err)
|
assert(validateApiSocketPacket(data))
|
||||||
|
|
||||||
|
Log.trace("Received websocket packet: {:#?}", data)
|
||||||
|
|
||||||
|
local handler = packetHandlers[data.packetType]
|
||||||
|
if handler then
|
||||||
|
local ok, err = pcall(handler, data.body)
|
||||||
|
if not ok then
|
||||||
|
Log.error("Error in WebSocket packet handler for type '%s': %s", data.packetType, err)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Log.warn("No handler for WebSocket packet type '%s'", data.packetType)
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Log.trace("Tracking request {}", request)
|
closed = self.__wsClient.Closed:Connect(function()
|
||||||
self.__activeRequests[request] = true
|
closed:Disconnect()
|
||||||
|
errored:Disconnect()
|
||||||
|
received:Disconnect()
|
||||||
|
|
||||||
return request:finally(function(...)
|
if self.__connected then
|
||||||
Log.trace("Cleaning up request {}", request)
|
reject("WebSocket connection closed unexpectedly")
|
||||||
self.__activeRequests[request] = nil
|
else
|
||||||
return ...
|
resolve()
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
errored = self.__wsClient.Error:Connect(function(code, msg)
|
||||||
if body.sessionId ~= self.__sessionId then
|
closed:Disconnect()
|
||||||
return Promise.reject("Server changed ID")
|
errored:Disconnect()
|
||||||
end
|
received:Disconnect()
|
||||||
|
|
||||||
assert(validateApiSubscribe(body))
|
reject("WebSocket error: " .. code .. " - " .. msg)
|
||||||
|
end)
|
||||||
self:setMessageCursor(body.messageCursor)
|
|
||||||
|
|
||||||
return body.messages
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -239,4 +289,40 @@ function ApiContext:open(id)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ApiContext:serialize(ids: { string })
|
||||||
|
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
||||||
|
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||||
|
|
||||||
|
return Http.post(url, request_body)
|
||||||
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.json)
|
||||||
|
:andThen(function(response_body)
|
||||||
|
if response_body.sessionId ~= self.__sessionId then
|
||||||
|
return Promise.reject("Server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(validateApiSerialize(response_body))
|
||||||
|
|
||||||
|
return response_body
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:refPatch(ids: { string })
|
||||||
|
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
||||||
|
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||||
|
|
||||||
|
return Http.post(url, request_body)
|
||||||
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.json)
|
||||||
|
:andThen(function(response_body)
|
||||||
|
if response_body.sessionId ~= self.__sessionId then
|
||||||
|
return Promise.reject("Server changed ID")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(validateApiRefPatch(response_body))
|
||||||
|
|
||||||
|
return response_body
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
return ApiContext
|
return ApiContext
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ end
|
|||||||
|
|
||||||
function Checkbox:render()
|
function Checkbox:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Checkbox
|
local checkboxTheme = theme.Checkbox
|
||||||
|
|
||||||
local activeTransparency = Roact.joinBindings({
|
local activeTransparency = Roact.joinBindings({
|
||||||
self.binding:map(function(value)
|
self.binding:map(function(value)
|
||||||
@@ -57,20 +57,21 @@ function Checkbox:render()
|
|||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
StateTip = e(Tooltip.Trigger, {
|
StateTip = e(Tooltip.Trigger, {
|
||||||
text = (if self.props.locked then "[LOCKED] " else "")
|
text = (if self.props.locked
|
||||||
.. (if self.props.active then "Enabled" else "Disabled"),
|
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n"
|
||||||
|
else "") .. (if self.props.active then "Enabled" else "Disabled"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Active = e(SlicedImage, {
|
Active = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.Active.BackgroundColor,
|
color = checkboxTheme.Active.BackgroundColor,
|
||||||
transparency = activeTransparency,
|
transparency = activeTransparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
||||||
ImageColor3 = theme.Active.IconColor,
|
ImageColor3 = checkboxTheme.Active.IconColor,
|
||||||
ImageTransparency = activeTransparency,
|
ImageTransparency = activeTransparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 16, 0, 16),
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
@@ -83,7 +84,7 @@ function Checkbox:render()
|
|||||||
|
|
||||||
Inactive = e(SlicedImage, {
|
Inactive = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = theme.Inactive.BorderColor,
|
color = checkboxTheme.Inactive.BorderColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
@@ -91,7 +92,7 @@ function Checkbox:render()
|
|||||||
Image = if self.props.locked
|
Image = if self.props.locked
|
||||||
then Assets.Images.Checkbox.Locked
|
then Assets.Images.Checkbox.Locked
|
||||||
else Assets.Images.Checkbox.Inactive,
|
else Assets.Images.Checkbox.Inactive,
|
||||||
ImageColor3 = theme.Inactive.IconColor,
|
ImageColor3 = checkboxTheme.Inactive.IconColor,
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 16, 0, 16),
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
local StudioService = game:GetService("StudioService")
|
local StudioService = game:GetService("StudioService")
|
||||||
local AssetService = game:GetService("AssetService")
|
local AssetService = game:GetService("AssetService")
|
||||||
|
|
||||||
|
type CachedImageInfo = {
|
||||||
|
pixels: buffer,
|
||||||
|
size: Vector2,
|
||||||
|
}
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -11,44 +16,71 @@ local e = Roact.createElement
|
|||||||
|
|
||||||
local EditableImage = require(Plugin.App.Components.EditableImage)
|
local EditableImage = require(Plugin.App.Components.EditableImage)
|
||||||
|
|
||||||
local imageCache = {}
|
local imageCache: { [string]: CachedImageInfo } = {}
|
||||||
local function getImageSizeAndPixels(image)
|
|
||||||
if not imageCache[image] then
|
local function cloneBuffer(b: buffer): buffer
|
||||||
local editableImage = AssetService:CreateEditableImageAsync(image)
|
local newBuffer = buffer.create(buffer.len(b))
|
||||||
|
buffer.copy(newBuffer, 0, b)
|
||||||
|
return newBuffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getImageSizeAndPixels(image: string): (Vector2, buffer)
|
||||||
|
local cachedImage = imageCache[image]
|
||||||
|
|
||||||
|
if not cachedImage then
|
||||||
|
local editableImage = AssetService:CreateEditableImageAsync(Content.fromUri(image))
|
||||||
|
local size = editableImage.Size
|
||||||
|
local pixels = editableImage:ReadPixelsBuffer(Vector2.zero, size)
|
||||||
imageCache[image] = {
|
imageCache[image] = {
|
||||||
Size = editableImage.Size,
|
pixels = pixels,
|
||||||
Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
|
size = size,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return size, cloneBuffer(pixels)
|
||||||
end
|
end
|
||||||
|
|
||||||
return imageCache[image].Size, table.clone(imageCache[image].Pixels)
|
return cachedImage.size, cloneBuffer(cachedImage.pixels)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getRecoloredClassIcon(className, color)
|
local function getRecoloredClassIcon(className, color)
|
||||||
local iconProps = StudioService:GetClassIcon(className)
|
local iconProps = StudioService:GetClassIcon(className)
|
||||||
|
|
||||||
if iconProps and color then
|
if iconProps and color then
|
||||||
local success, editableImageSize, editableImagePixels = pcall(function()
|
--stylua: ignore
|
||||||
local size, pixels = getImageSizeAndPixels(iconProps.Image)
|
local success, editableImageSize, editableImagePixels = pcall(function(_iconProps: { [any]: any }, _color: Color3): (Vector2, buffer)
|
||||||
|
local size, pixels = getImageSizeAndPixels(_iconProps.Image)
|
||||||
|
local pixelsLen = buffer.len(pixels)
|
||||||
|
|
||||||
local minVal, maxVal = math.huge, -math.huge
|
local minVal, maxVal = math.huge, -math.huge
|
||||||
for i = 1, #pixels, 4 do
|
|
||||||
if pixels[i + 3] == 0 then
|
for i = 0, pixelsLen, 4 do
|
||||||
|
if buffer.readu8(pixels, i + 3) == 0 then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
local pixelVal = math.max(
|
||||||
|
buffer.readu8(pixels, i),
|
||||||
|
buffer.readu8(pixels, i + 1),
|
||||||
|
buffer.readu8(pixels, i + 2)
|
||||||
|
)
|
||||||
|
|
||||||
minVal = math.min(minVal, pixelVal)
|
minVal = math.min(minVal, pixelVal)
|
||||||
maxVal = math.max(maxVal, pixelVal)
|
maxVal = math.max(maxVal, pixelVal)
|
||||||
end
|
end
|
||||||
|
|
||||||
local hue, sat, val = color:ToHSV()
|
local hue, sat, val = _color:ToHSV()
|
||||||
for i = 1, #pixels, 4 do
|
|
||||||
if pixels[i + 3] == 0 then
|
for i = 0, pixelsLen, 4 do
|
||||||
|
if buffer.readu8(pixels, i + 3) == 0 then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
local gIndex = i + 1
|
||||||
|
local bIndex = i + 2
|
||||||
|
|
||||||
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
local pixelVal = math.max(
|
||||||
|
buffer.readu8(pixels, i),
|
||||||
|
buffer.readu8(pixels, gIndex),
|
||||||
|
buffer.readu8(pixels, bIndex)
|
||||||
|
)
|
||||||
local newVal = val
|
local newVal = val
|
||||||
if minVal < maxVal then
|
if minVal < maxVal then
|
||||||
-- Remap minVal - maxVal to val*0.9 - val
|
-- Remap minVal - maxVal to val*0.9 - val
|
||||||
@@ -56,10 +88,12 @@ local function getRecoloredClassIcon(className, color)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
|
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
|
||||||
pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
|
buffer.writeu8(pixels, i, newPixelColor.R)
|
||||||
|
buffer.writeu8(pixels, gIndex, newPixelColor.G)
|
||||||
|
buffer.writeu8(pixels, bIndex, newPixelColor.B)
|
||||||
end
|
end
|
||||||
return size, pixels
|
return size, pixels
|
||||||
end)
|
end, iconProps, color)
|
||||||
if success then
|
if success then
|
||||||
iconProps.EditableImagePixels = editableImagePixels
|
iconProps.EditableImagePixels = editableImagePixels
|
||||||
iconProps.EditableImageSize = editableImageSize
|
iconProps.EditableImageSize = editableImageSize
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
|
||||||
local Packages = Rojo.Packages
|
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
|
||||||
local Highlighter = require(Packages.Highlighter)
|
|
||||||
Highlighter.matchStudioSettings()
|
|
||||||
|
|
||||||
local e = Roact.createElement
|
|
||||||
|
|
||||||
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
|
|
||||||
|
|
||||||
function CodeLabel:init()
|
|
||||||
self.labelRef = Roact.createRef()
|
|
||||||
self.highlightsRef = Roact.createRef()
|
|
||||||
end
|
|
||||||
|
|
||||||
function CodeLabel:didMount()
|
|
||||||
Highlighter.highlight({
|
|
||||||
textObject = self.labelRef:getValue(),
|
|
||||||
})
|
|
||||||
self:updateHighlights()
|
|
||||||
end
|
|
||||||
|
|
||||||
function CodeLabel:didUpdate()
|
|
||||||
self:updateHighlights()
|
|
||||||
end
|
|
||||||
|
|
||||||
function CodeLabel:updateHighlights()
|
|
||||||
local highlights = self.highlightsRef:getValue()
|
|
||||||
if not highlights then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, lineLabel in highlights:GetChildren() do
|
|
||||||
local lineNum = tonumber(string.match(lineLabel.Name, "%d+") or "0")
|
|
||||||
lineLabel.BackgroundColor3 = self.props.lineBackground
|
|
||||||
lineLabel.BorderSizePixel = 0
|
|
||||||
lineLabel.BackgroundTransparency = if self.props.markedLines[lineNum] then 0.25 else 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function CodeLabel:render()
|
|
||||||
return e("TextLabel", {
|
|
||||||
Size = self.props.size,
|
|
||||||
Position = self.props.position,
|
|
||||||
Text = self.props.text,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
Font = Enum.Font.RobotoMono,
|
|
||||||
TextSize = 16,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
TextYAlignment = Enum.TextYAlignment.Top,
|
|
||||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
|
||||||
[Roact.Ref] = self.labelRef,
|
|
||||||
}, {
|
|
||||||
SyntaxHighlights = e("Folder", {
|
|
||||||
[Roact.Ref] = self.highlightsRef,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return CodeLabel
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -10,9 +8,11 @@ local Flipper = require(Packages.Flipper)
|
|||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local SlicedImage = require(script.Parent.SlicedImage)
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
||||||
|
local Tooltip = require(script.Parent.Tooltip)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -44,29 +44,29 @@ end
|
|||||||
|
|
||||||
function Dropdown:render()
|
function Dropdown:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Dropdown
|
local dropdownTheme = theme.Dropdown
|
||||||
|
|
||||||
local optionButtons = {}
|
local optionButtons = {}
|
||||||
local width = -1
|
local width = -1
|
||||||
for i, option in self.props.options do
|
for i, option in self.props.options do
|
||||||
local text = tostring(option or "")
|
local text = tostring(option or "")
|
||||||
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
|
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge)
|
||||||
if textSize.X > width then
|
if textBounds.X > width then
|
||||||
width = textSize.X
|
width = textBounds.X
|
||||||
end
|
end
|
||||||
|
|
||||||
optionButtons[text] = e("TextButton", {
|
optionButtons[text] = e("TextButton", {
|
||||||
Text = text,
|
Text = text,
|
||||||
LayoutOrder = i,
|
LayoutOrder = i,
|
||||||
Size = UDim2.new(1, 0, 0, 24),
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
BackgroundColor3 = theme.BackgroundColor,
|
BackgroundColor3 = dropdownTheme.BackgroundColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
BackgroundTransparency = self.props.transparency,
|
BackgroundTransparency = self.props.transparency,
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = dropdownTheme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
if self.props.locked then
|
if self.props.locked then
|
||||||
@@ -103,13 +103,13 @@ function Dropdown:render()
|
|||||||
}, {
|
}, {
|
||||||
Border = e(SlicedImage, {
|
Border = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = theme.BorderColor,
|
color = dropdownTheme.BorderColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
DropArrow = e("ImageLabel", {
|
DropArrow = e("ImageLabel", {
|
||||||
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
||||||
ImageColor3 = theme.IconColor,
|
ImageColor3 = dropdownTheme.IconColor,
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 18, 0, 18),
|
Size = UDim2.new(0, 18, 0, 18),
|
||||||
@@ -120,15 +120,21 @@ function Dropdown:render()
|
|||||||
end),
|
end),
|
||||||
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
StateTip = if self.props.locked
|
||||||
|
then e(Tooltip.Trigger, {
|
||||||
|
text = self.props.lockedTooltip or "(Cannot be changed right now)",
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
}),
|
}),
|
||||||
Active = e("TextLabel", {
|
Active = e("TextLabel", {
|
||||||
Size = UDim2.new(1, -30, 1, 0),
|
Size = UDim2.new(1, -30, 1, 0),
|
||||||
Position = UDim2.new(0, 6, 0, 0),
|
Position = UDim2.new(0, 6, 0, 0),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = self.props.active,
|
Text = self.props.active,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = dropdownTheme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
}),
|
}),
|
||||||
@@ -136,7 +142,7 @@ function Dropdown:render()
|
|||||||
Options = if self.state.open
|
Options = if self.state.open
|
||||||
then e(SlicedImage, {
|
then e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.BackgroundColor,
|
color = dropdownTheme.BackgroundColor,
|
||||||
position = UDim2.new(1, 0, 1, 3),
|
position = UDim2.new(1, 0, 1, 3),
|
||||||
size = self.openBinding:map(function(a)
|
size = self.openBinding:map(function(a)
|
||||||
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
||||||
@@ -145,7 +151,7 @@ function Dropdown:render()
|
|||||||
}, {
|
}, {
|
||||||
Border = e(SlicedImage, {
|
Border = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = theme.BorderColor,
|
color = dropdownTheme.BorderColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ function EditableImage:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function EditableImage:writePixels()
|
function EditableImage:writePixels()
|
||||||
local image = self.ref.current
|
local image = self.ref.current :: EditableImage
|
||||||
|
|
||||||
if not image then
|
if not image then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -20,7 +21,7 @@ function EditableImage:writePixels()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
|
image:WritePixelsBuffer(Vector2.zero, self.props.size, self.props.pixels)
|
||||||
end
|
end
|
||||||
|
|
||||||
function EditableImage:render()
|
function EditableImage:render()
|
||||||
|
|||||||
@@ -9,8 +9,70 @@ local Assets = require(Plugin.Assets)
|
|||||||
local Config = require(Plugin.Config)
|
local Config = require(Plugin.Config)
|
||||||
local Version = require(Plugin.Version)
|
local Version = require(Plugin.Version)
|
||||||
|
|
||||||
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local function VersionIndicator(props)
|
||||||
|
local updateMessage = Version.getUpdateMessage()
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e("Frame", {
|
||||||
|
LayoutOrder = props.layoutOrder,
|
||||||
|
Size = UDim2.new(0, 0, 0, 25),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
}, {
|
||||||
|
Border = if updateMessage
|
||||||
|
then e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBorder,
|
||||||
|
color = theme.Button.Bordered.Enabled.BorderColor,
|
||||||
|
transparency = props.transparency,
|
||||||
|
size = UDim2.fromScale(1, 1),
|
||||||
|
zIndex = 0,
|
||||||
|
}, {
|
||||||
|
Indicator = e("ImageLabel", {
|
||||||
|
Size = UDim2.new(0, 10, 0, 10),
|
||||||
|
ScaleType = Enum.ScaleType.Fit,
|
||||||
|
Image = Assets.Images.Circles[16],
|
||||||
|
ImageColor3 = theme.Header.LogoColor,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Position = UDim2.new(1, 0, 0, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
|
||||||
|
Tip = if updateMessage
|
||||||
|
then e(Tooltip.Trigger, {
|
||||||
|
text = updateMessage,
|
||||||
|
delay = 0.1,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
|
||||||
|
VersionText = e("TextLabel", {
|
||||||
|
Text = Version.display(Config.version),
|
||||||
|
FontFace = theme.Font.Thin,
|
||||||
|
TextSize = theme.TextSize.Body,
|
||||||
|
TextColor3 = theme.Header.VersionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 6),
|
||||||
|
PaddingRight = UDim.new(0, 6),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local function Header(props)
|
local function Header(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
@@ -29,18 +91,9 @@ local function Header(props)
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Version = e("TextLabel", {
|
VersionIndicator = e(VersionIndicator, {
|
||||||
Text = Version.display(Config.version),
|
transparency = props.transparency,
|
||||||
Font = Enum.Font.Gotham,
|
layoutOrder = 2,
|
||||||
TextSize = 14,
|
|
||||||
TextColor3 = theme.Header.VersionColor,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
TextTransparency = props.transparency,
|
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 14),
|
|
||||||
|
|
||||||
LayoutOrder = 2,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
local StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotificationnNotification")
|
||||||
|
|
||||||
|
function FullscreenNotification:init()
|
||||||
|
self.transparency, self.setTransparency = Roact.createBinding(0)
|
||||||
|
self.lifetime = self.props.timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
function FullscreenNotification:dismiss()
|
||||||
|
if self.props.onClose then
|
||||||
|
self.props.onClose()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function FullscreenNotification:didMount()
|
||||||
|
self.props.soundPlayer:play(Assets.Sounds.Notification)
|
||||||
|
|
||||||
|
self.timeout = task.spawn(function()
|
||||||
|
local clock = os.clock()
|
||||||
|
local seen = false
|
||||||
|
while task.wait(1 / 10) do
|
||||||
|
local now = os.clock()
|
||||||
|
local dt = now - clock
|
||||||
|
clock = now
|
||||||
|
|
||||||
|
if not seen then
|
||||||
|
seen = StudioService.ActiveScript == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if not seen then
|
||||||
|
-- Don't run down timer before being viewed
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
self.lifetime -= dt
|
||||||
|
if self.lifetime <= 0 then
|
||||||
|
self:dismiss()
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.timeout = nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function FullscreenNotification:willUnmount()
|
||||||
|
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||||
|
task.cancel(self.timeout)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function FullscreenNotification:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local actionButtons = {}
|
||||||
|
if self.props.actions then
|
||||||
|
for key, action in self.props.actions do
|
||||||
|
actionButtons[key] = e(TextButton, {
|
||||||
|
text = action.text,
|
||||||
|
style = action.style,
|
||||||
|
onClick = function()
|
||||||
|
self:dismiss()
|
||||||
|
if action.onClick then
|
||||||
|
local success, err = pcall(action.onClick, self)
|
||||||
|
if not success then
|
||||||
|
Log.warn("Error in notification action: " .. tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
layoutOrder = -action.layoutOrder,
|
||||||
|
transparency = self.transparency,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
BackgroundColor3 = theme.BackgroundColor,
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
ZIndex = self.props.layoutOrder,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 17),
|
||||||
|
PaddingRight = UDim.new(0, 15),
|
||||||
|
PaddingTop = UDim.new(0, 10),
|
||||||
|
PaddingBottom = UDim.new(0, 10),
|
||||||
|
}),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
Padding = UDim.new(0, 10),
|
||||||
|
}),
|
||||||
|
Logo = e("ImageLabel", {
|
||||||
|
ImageTransparency = self.transparency,
|
||||||
|
Image = Assets.Images.Logo,
|
||||||
|
ImageColor3 = theme.Header.LogoColor,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.fromOffset(60, 27),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Info = e("TextLabel", {
|
||||||
|
Text = self.props.text,
|
||||||
|
FontFace = theme.Font.Main,
|
||||||
|
TextSize = theme.TextSize.Body,
|
||||||
|
TextColor3 = theme.Notification.InfoColor,
|
||||||
|
TextTransparency = self.transparency,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Center,
|
||||||
|
TextWrapped = true,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
AutomaticSize = Enum.AutomaticSize.Y,
|
||||||
|
Size = UDim2.fromScale(0.4, 0),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
Actions = if self.props.actions
|
||||||
|
then e("Frame", {
|
||||||
|
Size = UDim2.new(1, -40, 0, 37),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 3,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Buttons = Roact.createFragment(actionButtons),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return FullscreenNotification
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
local StudioService = game:GetService("StudioService")
|
local StudioService = game:GetService("StudioService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
@@ -9,16 +8,14 @@ local Roact = require(Packages.Roact)
|
|||||||
local Flipper = require(Packages.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
local bindingUtil = require(script.Parent.bindingUtil)
|
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
|
||||||
local baseClock = DateTime.now().UnixTimestampMillis
|
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local Notification = Roact.Component:extend("Notification")
|
local Notification = Roact.Component:extend("Notification")
|
||||||
@@ -78,7 +75,9 @@ function Notification:didMount()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Notification:willUnmount()
|
function Notification:willUnmount()
|
||||||
task.cancel(self.timeout)
|
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||||
|
task.cancel(self.timeout)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:render()
|
function Notification:render()
|
||||||
@@ -86,51 +85,49 @@ function Notification:render()
|
|||||||
return 1 - value
|
return 1 - value
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
|
return Theme.with(function(theme)
|
||||||
|
local actionButtons = {}
|
||||||
|
local buttonsX = 0
|
||||||
|
if self.props.actions then
|
||||||
|
local count = 0
|
||||||
|
for key, action in self.props.actions do
|
||||||
|
actionButtons[key] = e(TextButton, {
|
||||||
|
text = action.text,
|
||||||
|
style = action.style,
|
||||||
|
onClick = function()
|
||||||
|
self:dismiss()
|
||||||
|
if action.onClick then
|
||||||
|
local success, err = pcall(action.onClick, self)
|
||||||
|
if not success then
|
||||||
|
Log.warn("Error in notification action: " .. tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
layoutOrder = -action.layoutOrder,
|
||||||
|
transparency = transparency,
|
||||||
|
})
|
||||||
|
|
||||||
local actionButtons = {}
|
buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2)
|
||||||
local buttonsX = 0
|
|
||||||
if self.props.actions then
|
|
||||||
local count = 0
|
|
||||||
for key, action in self.props.actions do
|
|
||||||
actionButtons[key] = e(TextButton, {
|
|
||||||
text = action.text,
|
|
||||||
style = action.style,
|
|
||||||
onClick = function()
|
|
||||||
local success, err = pcall(action.onClick, self)
|
|
||||||
if not success then
|
|
||||||
Log.warn("Error in notification action: " .. tostring(err))
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
layoutOrder = -action.layoutOrder,
|
|
||||||
transparency = transparency,
|
|
||||||
})
|
|
||||||
|
|
||||||
buttonsX += TextService:GetTextSize(
|
count += 1
|
||||||
action.text,
|
end
|
||||||
18,
|
|
||||||
Enum.Font.GothamMedium,
|
|
||||||
Vector2.new(math.huge, math.huge)
|
|
||||||
).X + 30
|
|
||||||
|
|
||||||
count += 1
|
buttonsX += (count - 1) * 5
|
||||||
end
|
end
|
||||||
|
|
||||||
buttonsX += (count - 1) * 5
|
local paddingY, logoSize = 20, 32
|
||||||
end
|
local actionsY = if self.props.actions then 37 else 0
|
||||||
|
local textXSpace = math.max(250, buttonsX) + 35
|
||||||
|
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Body, textXSpace)
|
||||||
|
local contentX = math.max(textBounds.X, buttonsX)
|
||||||
|
|
||||||
local paddingY, logoSize = 20, 32
|
local size = self.binding:map(function(value)
|
||||||
local actionsY = if self.props.actions then 35 else 0
|
return UDim2.fromOffset(
|
||||||
local contentX = math.max(textBounds.X, buttonsX)
|
(35 + 40 + contentX) * value,
|
||||||
|
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
local size = self.binding:map(function(value)
|
|
||||||
return UDim2.fromOffset(
|
|
||||||
(35 + 40 + contentX) * value,
|
|
||||||
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
|
||||||
return e("TextButton", {
|
return e("TextButton", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = size,
|
Size = size,
|
||||||
@@ -144,31 +141,31 @@ function Notification:render()
|
|||||||
}, {
|
}, {
|
||||||
e(BorderedContainer, {
|
e(BorderedContainer, {
|
||||||
transparency = transparency,
|
transparency = transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.fromScale(1, 1),
|
||||||
}, {
|
}, {
|
||||||
Contents = e("Frame", {
|
Contents = e("Frame", {
|
||||||
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
Size = UDim2.fromScale(1, 1),
|
||||||
Position = UDim2.new(0, 0, 0, paddingY / 2),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Logo = e("ImageLabel", {
|
Logo = e("ImageLabel", {
|
||||||
ImageTransparency = transparency,
|
ImageTransparency = transparency,
|
||||||
Image = Assets.Images.PluginButton,
|
Image = Assets.Images.PluginButton,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0, logoSize, 0, logoSize),
|
Size = UDim2.fromOffset(logoSize, logoSize),
|
||||||
Position = UDim2.new(0, 0, 0, 0),
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
AnchorPoint = Vector2.new(0, 0),
|
AnchorPoint = Vector2.new(0, 0),
|
||||||
}),
|
}),
|
||||||
Info = e("TextLabel", {
|
Info = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Notification.InfoColor,
|
TextColor3 = theme.Notification.InfoColor,
|
||||||
TextTransparency = transparency,
|
TextTransparency = transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Center,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
|
|
||||||
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
|
Size = UDim2.new(0, textBounds.X, 1, -actionsY),
|
||||||
Position = UDim2.fromOffset(35, 0),
|
Position = UDim2.fromOffset(35, 0),
|
||||||
|
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
@@ -176,8 +173,8 @@ function Notification:render()
|
|||||||
}),
|
}),
|
||||||
Actions = if self.props.actions
|
Actions = if self.props.actions
|
||||||
then e("Frame", {
|
then e("Frame", {
|
||||||
Size = UDim2.new(1, -40, 0, 35),
|
Size = UDim2.new(1, -40, 0, actionsY),
|
||||||
Position = UDim2.new(1, 0, 1, 0),
|
Position = UDim2.fromScale(1, 1),
|
||||||
AnchorPoint = Vector2.new(1, 1),
|
AnchorPoint = Vector2.new(1, 1),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
@@ -196,32 +193,12 @@ function Notification:render()
|
|||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
PaddingLeft = UDim.new(0, 17),
|
PaddingLeft = UDim.new(0, 17),
|
||||||
PaddingRight = UDim.new(0, 15),
|
PaddingRight = UDim.new(0, 15),
|
||||||
|
PaddingTop = UDim.new(0, paddingY / 2),
|
||||||
|
PaddingBottom = UDim.new(0, paddingY / 2),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local Notifications = Roact.Component:extend("Notifications")
|
return Notification
|
||||||
|
|
||||||
function Notifications:render()
|
|
||||||
local notifs = {}
|
|
||||||
|
|
||||||
for id, notif in self.props.notifications do
|
|
||||||
notifs["NotifID_" .. id] = e(Notification, {
|
|
||||||
soundPlayer = self.props.soundPlayer,
|
|
||||||
text = notif.text,
|
|
||||||
timestamp = notif.timestamp,
|
|
||||||
timeout = notif.timeout,
|
|
||||||
actions = notif.actions,
|
|
||||||
layoutOrder = (notif.timestamp - baseClock),
|
|
||||||
onClose = function()
|
|
||||||
self.props.onClose(id)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return Roact.createFragment(notifs)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Notifications
|
|
||||||
66
plugin/src/App/Components/Notifications/init.lua
Normal file
66
plugin/src/App/Components/Notifications/init.lua
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Notification = require(script.Notification)
|
||||||
|
local FullscreenNotification = require(script.FullscreenNotification)
|
||||||
|
|
||||||
|
local Notifications = Roact.Component:extend("Notifications")
|
||||||
|
|
||||||
|
function Notifications:render()
|
||||||
|
local popupNotifs = {}
|
||||||
|
local fullscreenNotifs = {}
|
||||||
|
|
||||||
|
for id, notif in self.props.notifications do
|
||||||
|
local targetTable = if notif.isFullscreen then fullscreenNotifs else popupNotifs
|
||||||
|
local targetComponent = if notif.isFullscreen then FullscreenNotification else Notification
|
||||||
|
targetTable["NotifID_" .. id] = e(targetComponent, {
|
||||||
|
soundPlayer = self.props.soundPlayer,
|
||||||
|
text = notif.text,
|
||||||
|
timeout = notif.timeout,
|
||||||
|
actions = notif.actions,
|
||||||
|
layoutOrder = id,
|
||||||
|
onClose = function()
|
||||||
|
if notif.onClose then
|
||||||
|
notif.onClose()
|
||||||
|
end
|
||||||
|
self.props.onClose(id)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Fullscreen = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
notifs = Roact.createFragment(fullscreenNotifs),
|
||||||
|
}),
|
||||||
|
Popups = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
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 = Roact.createFragment(popupNotifs),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return Notifications
|
||||||
@@ -39,8 +39,8 @@ local function ViewDiffButton(props)
|
|||||||
Label = e("TextLabel", {
|
Label = e("TextLabel", {
|
||||||
Text = "View Diff",
|
Text = "View Diff",
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -170,8 +170,8 @@ function ChangeList:render()
|
|||||||
ColumnA = e("TextLabel", {
|
ColumnA = e("TextLabel", {
|
||||||
Text = tostring(headerRow[1]),
|
Text = tostring(headerRow[1]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -182,8 +182,8 @@ function ChangeList:render()
|
|||||||
ColumnB = e("TextLabel", {
|
ColumnB = e("TextLabel", {
|
||||||
Text = tostring(headerRow[2]),
|
Text = tostring(headerRow[2]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -194,8 +194,8 @@ function ChangeList:render()
|
|||||||
ColumnC = e("TextLabel", {
|
ColumnC = e("TextLabel", {
|
||||||
Text = tostring(headerRow[3]),
|
Text = tostring(headerRow[3]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -230,8 +230,8 @@ function ChangeList:render()
|
|||||||
ColumnA = e("TextLabel", {
|
ColumnA = e("TextLabel", {
|
||||||
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ local function DisplayValue(props)
|
|||||||
Label = e("TextLabel", {
|
Label = e("TextLabel", {
|
||||||
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = props.textColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -90,8 +90,8 @@ local function DisplayValue(props)
|
|||||||
return e("TextLabel", {
|
return e("TextLabel", {
|
||||||
Text = textRepresentation,
|
Text = textRepresentation,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = props.textColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -112,8 +112,8 @@ local function DisplayValue(props)
|
|||||||
return e("TextLabel", {
|
return e("TextLabel", {
|
||||||
Text = textRepresentation,
|
Text = textRepresentation,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = props.textColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function DomLabel:render()
|
|||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local color = if props.isWarning
|
local color = if props.isWarning
|
||||||
then theme.Diff.Warning
|
then theme.Diff.Warning
|
||||||
elseif props.patchType then theme.Diff[props.patchType]
|
elseif props.patchType then theme.Diff.Background[props.patchType]
|
||||||
else theme.TextColor
|
else theme.TextColor
|
||||||
|
|
||||||
local indent = (depth - 1) * 12 + 15
|
local indent = (depth - 1) * 12 + 15
|
||||||
@@ -225,8 +225,8 @@ function DomLabel:render()
|
|||||||
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
||||||
RichText = true,
|
RichText = true,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
|
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = color,
|
TextColor3 = color,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -251,11 +251,11 @@ function DomLabel:render()
|
|||||||
then e("TextLabel", {
|
then e("TextLabel", {
|
||||||
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.SubTextColor,
|
TextColor3 = theme.SubTextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
Size = UDim2.new(0, 0, 0, 16),
|
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||||
AutomaticSize = Enum.AutomaticSize.X,
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
})
|
})
|
||||||
@@ -264,11 +264,11 @@ function DomLabel:render()
|
|||||||
then e("TextLabel", {
|
then e("TextLabel", {
|
||||||
Text = props.changeInfo.failed,
|
Text = props.changeInfo.failed,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Diff.Warning,
|
TextColor3 = theme.Diff.Warning,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
Size = UDim2.new(0, 0, 0, 16),
|
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||||
AutomaticSize = Enum.AutomaticSize.X,
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
LayoutOrder = 6,
|
LayoutOrder = 6,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ function PatchVisualizer:render()
|
|||||||
CleanMerge = e("TextLabel", {
|
CleanMerge = e("TextLabel", {
|
||||||
Visible = #scrollElements == 0,
|
Visible = #scrollElements == 0,
|
||||||
Text = "No changes to sync, project is up to date.",
|
Text = "No changes to sync, project is up to date.",
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
--!strict
|
||||||
--[[
|
--[[
|
||||||
Based on DiffMatchPatch by Neil Fraser.
|
Based on DiffMatchPatch by Neil Fraser.
|
||||||
https://github.com/google/diff-match-patch
|
https://github.com/google/diff-match-patch
|
||||||
@@ -67,8 +68,187 @@ function StringDiff.findDiffs(text1: string, text2: string): Diffs
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Cleanup the diff
|
-- Cleanup the diff
|
||||||
|
diffs = StringDiff._cleanupSemantic(diffs)
|
||||||
diffs = StringDiff._reorderAndMerge(diffs)
|
diffs = StringDiff._reorderAndMerge(diffs)
|
||||||
|
|
||||||
|
-- Remove any empty diffs
|
||||||
|
local cursor = 1
|
||||||
|
while cursor and diffs[cursor] do
|
||||||
|
if diffs[cursor].value == "" then
|
||||||
|
table.remove(diffs, cursor)
|
||||||
|
else
|
||||||
|
cursor += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._computeDiff(text1: string, text2: string): Diffs
|
||||||
|
-- Assumes that the prefix and suffix have already been trimmed off
|
||||||
|
-- and shortcut returns have been made so these texts must be different
|
||||||
|
|
||||||
|
local text1Length, text2Length = #text1, #text2
|
||||||
|
|
||||||
|
if text1Length == 0 then
|
||||||
|
-- It's simply inserting all of text2 into text1
|
||||||
|
return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
|
||||||
|
end
|
||||||
|
|
||||||
|
if text2Length == 0 then
|
||||||
|
-- It's simply deleting all of text1
|
||||||
|
return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
|
||||||
|
end
|
||||||
|
|
||||||
|
local longText = if text1Length > text2Length then text1 else text2
|
||||||
|
local shortText = if text1Length > text2Length then text2 else text1
|
||||||
|
local shortTextLength = #shortText
|
||||||
|
|
||||||
|
-- Shortcut if the shorter string exists entirely inside the longer one
|
||||||
|
local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true)
|
||||||
|
if indexOf ~= nil then
|
||||||
|
local diffs = {
|
||||||
|
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) },
|
||||||
|
{ actionType = StringDiff.ActionTypes.Equal, value = shortText },
|
||||||
|
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) },
|
||||||
|
}
|
||||||
|
-- Swap insertions for deletions if diff is reversed
|
||||||
|
if text1Length > text2Length then
|
||||||
|
diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete
|
||||||
|
end
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
if shortTextLength == 1 then
|
||||||
|
-- Single character string
|
||||||
|
-- After the previous shortcut, the character can't be an equality
|
||||||
|
return {
|
||||||
|
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
||||||
|
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiff._bisect(text1, text2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._cleanupSemantic(diffs: Diffs): Diffs
|
||||||
|
-- Reduce the number of edits by eliminating semantically trivial equalities.
|
||||||
|
local changes = false
|
||||||
|
local equalities = {} -- Stack of indices where equalities are found.
|
||||||
|
local equalitiesLength = 0 -- Keeping our own length var is faster.
|
||||||
|
local lastEquality: string? = nil
|
||||||
|
-- Always equal to diffs[equalities[equalitiesLength]].value
|
||||||
|
local pointer = 1 -- Index of current position.
|
||||||
|
-- Number of characters that changed prior to the equality.
|
||||||
|
local length_insertions1 = 0
|
||||||
|
local length_deletions1 = 0
|
||||||
|
-- Number of characters that changed after the equality.
|
||||||
|
local length_insertions2 = 0
|
||||||
|
local length_deletions2 = 0
|
||||||
|
|
||||||
|
while diffs[pointer] do
|
||||||
|
if diffs[pointer].actionType == StringDiff.ActionTypes.Equal then -- Equality found.
|
||||||
|
equalitiesLength = equalitiesLength + 1
|
||||||
|
equalities[equalitiesLength] = pointer
|
||||||
|
length_insertions1 = length_insertions2
|
||||||
|
length_deletions1 = length_deletions2
|
||||||
|
length_insertions2 = 0
|
||||||
|
length_deletions2 = 0
|
||||||
|
lastEquality = diffs[pointer].value
|
||||||
|
else -- An insertion or deletion.
|
||||||
|
if diffs[pointer].actionType == StringDiff.ActionTypes.Insert then
|
||||||
|
length_insertions2 = length_insertions2 + #diffs[pointer].value
|
||||||
|
else
|
||||||
|
length_deletions2 = length_deletions2 + #diffs[pointer].value
|
||||||
|
end
|
||||||
|
-- Eliminate an equality that is smaller or equal to the edits on both
|
||||||
|
-- sides of it.
|
||||||
|
if
|
||||||
|
lastEquality
|
||||||
|
and (#lastEquality <= math.max(length_insertions1, length_deletions1))
|
||||||
|
and (#lastEquality <= math.max(length_insertions2, length_deletions2))
|
||||||
|
then
|
||||||
|
-- Duplicate record.
|
||||||
|
table.insert(
|
||||||
|
diffs,
|
||||||
|
equalities[equalitiesLength],
|
||||||
|
{ actionType = StringDiff.ActionTypes.Delete, value = lastEquality }
|
||||||
|
)
|
||||||
|
-- Change second copy to insert.
|
||||||
|
diffs[equalities[equalitiesLength] + 1].actionType = StringDiff.ActionTypes.Insert
|
||||||
|
-- Throw away the equality we just deleted.
|
||||||
|
equalitiesLength = equalitiesLength - 1
|
||||||
|
-- Throw away the previous equality (it needs to be reevaluated).
|
||||||
|
equalitiesLength = equalitiesLength - 1
|
||||||
|
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
|
||||||
|
length_insertions1, length_deletions1 = 0, 0 -- Reset the counters.
|
||||||
|
length_insertions2, length_deletions2 = 0, 0
|
||||||
|
lastEquality = nil
|
||||||
|
changes = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Normalize the diff.
|
||||||
|
if changes then
|
||||||
|
StringDiff._reorderAndMerge(diffs)
|
||||||
|
end
|
||||||
|
StringDiff._cleanupSemanticLossless(diffs)
|
||||||
|
|
||||||
|
-- Find any overlaps between deletions and insertions.
|
||||||
|
-- e.g: <del>abcxxx</del><ins>xxxdef</ins>
|
||||||
|
-- -> <del>abc</del>xxx<ins>def</ins>
|
||||||
|
-- e.g: <del>xxxabc</del><ins>defxxx</ins>
|
||||||
|
-- -> <ins>def</ins>xxx<del>abc</del>
|
||||||
|
-- Only extract an overlap if it is as big as the edit ahead or behind it.
|
||||||
|
pointer = 2
|
||||||
|
while diffs[pointer] do
|
||||||
|
if
|
||||||
|
diffs[pointer - 1].actionType == StringDiff.ActionTypes.Delete
|
||||||
|
and diffs[pointer].actionType == StringDiff.ActionTypes.Insert
|
||||||
|
then
|
||||||
|
local deletion = diffs[pointer - 1].value
|
||||||
|
local insertion = diffs[pointer].value
|
||||||
|
local overlap_length1 = StringDiff._commonOverlap(deletion, insertion)
|
||||||
|
local overlap_length2 = StringDiff._commonOverlap(insertion, deletion)
|
||||||
|
if overlap_length1 >= overlap_length2 then
|
||||||
|
if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then
|
||||||
|
-- Overlap found. Insert an equality and trim the surrounding edits.
|
||||||
|
table.insert(
|
||||||
|
diffs,
|
||||||
|
pointer,
|
||||||
|
{ actionType = StringDiff.ActionTypes.Equal, value = string.sub(insertion, 1, overlap_length1) }
|
||||||
|
)
|
||||||
|
diffs[pointer - 1].value = string.sub(deletion, 1, #deletion - overlap_length1)
|
||||||
|
diffs[pointer + 1].value = string.sub(insertion, overlap_length1 + 1)
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then
|
||||||
|
-- Reverse overlap found.
|
||||||
|
-- Insert an equality and swap and trim the surrounding edits.
|
||||||
|
table.insert(
|
||||||
|
diffs,
|
||||||
|
pointer,
|
||||||
|
{ actionType = StringDiff.ActionTypes.Equal, value = string.sub(deletion, 1, overlap_length2) }
|
||||||
|
)
|
||||||
|
diffs[pointer - 1] = {
|
||||||
|
actionType = StringDiff.ActionTypes.Insert,
|
||||||
|
value = string.sub(insertion, 1, #insertion - overlap_length2),
|
||||||
|
}
|
||||||
|
diffs[pointer + 1] = {
|
||||||
|
actionType = StringDiff.ActionTypes.Delete,
|
||||||
|
value = string.sub(deletion, overlap_length2 + 1),
|
||||||
|
}
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
|
||||||
return diffs
|
return diffs
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -124,51 +304,164 @@ function StringDiff._sharedSuffix(text1: string, text2: string): number
|
|||||||
return pointerMid
|
return pointerMid
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiff._computeDiff(text1: string, text2: string): Diffs
|
function StringDiff._commonOverlap(text1: string, text2: string): number
|
||||||
-- Assumes that the prefix and suffix have already been trimmed off
|
-- Determine if the suffix of one string is the prefix of another.
|
||||||
-- and shortcut returns have been made so these texts must be different
|
|
||||||
|
|
||||||
local text1Length, text2Length = #text1, #text2
|
-- Cache the text lengths to prevent multiple calls.
|
||||||
|
local text1_length = #text1
|
||||||
if text1Length == 0 then
|
local text2_length = #text2
|
||||||
-- It's simply inserting all of text2 into text1
|
-- Eliminate the null case.
|
||||||
return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
|
if text1_length == 0 or text2_length == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
-- Truncate the longer string.
|
||||||
|
if text1_length > text2_length then
|
||||||
|
text1 = string.sub(text1, text1_length - text2_length + 1)
|
||||||
|
elseif text1_length < text2_length then
|
||||||
|
text2 = string.sub(text2, 1, text1_length)
|
||||||
|
end
|
||||||
|
local text_length = math.min(text1_length, text2_length)
|
||||||
|
-- Quick check for the worst case.
|
||||||
|
if text1 == text2 then
|
||||||
|
return text_length
|
||||||
end
|
end
|
||||||
|
|
||||||
if text2Length == 0 then
|
-- Start by looking for a single character match
|
||||||
-- It's simply deleting all of text1
|
-- and increase length until no match is found.
|
||||||
return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
|
-- Performance analysis: https://neil.fraser.name/news/2010/11/04/
|
||||||
end
|
local best = 0
|
||||||
|
local length = 1
|
||||||
local longText = if text1Length > text2Length then text1 else text2
|
while true do
|
||||||
local shortText = if text1Length > text2Length then text2 else text1
|
local pattern = string.sub(text1, text_length - length + 1)
|
||||||
local shortTextLength = #shortText
|
local found = string.find(text2, pattern, 1, true)
|
||||||
|
if found == nil then
|
||||||
-- Shortcut if the shorter string exists entirely inside the longer one
|
return best
|
||||||
local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true)
|
|
||||||
if indexOf ~= nil then
|
|
||||||
local diffs = {
|
|
||||||
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) },
|
|
||||||
{ actionType = StringDiff.ActionTypes.Equal, value = shortText },
|
|
||||||
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) },
|
|
||||||
}
|
|
||||||
-- Swap insertions for deletions if diff is reversed
|
|
||||||
if text1Length > text2Length then
|
|
||||||
diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete
|
|
||||||
end
|
end
|
||||||
return diffs
|
length = length + found - 1
|
||||||
|
if found == 1 or string.sub(text1, text_length - length + 1) == string.sub(text2, 1, length) then
|
||||||
|
best = length
|
||||||
|
length = length + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._cleanupSemanticScore(one: string, two: string): number
|
||||||
|
-- Given two strings, compute a score representing whether the internal
|
||||||
|
-- boundary falls on logical boundaries.
|
||||||
|
-- Scores range from 6 (best) to 0 (worst).
|
||||||
|
|
||||||
|
if (#one == 0) or (#two == 0) then
|
||||||
|
-- Edges are the best.
|
||||||
|
return 6
|
||||||
end
|
end
|
||||||
|
|
||||||
if shortTextLength == 1 then
|
-- Each port of this function behaves slightly differently due to
|
||||||
-- Single character string
|
-- subtle differences in each language's definition of things like
|
||||||
-- After the previous shortcut, the character can't be an equality
|
-- 'whitespace'. Since this function's purpose is largely cosmetic,
|
||||||
return {
|
-- the choice has been made to use each language's native features
|
||||||
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
-- rather than force total conformity.
|
||||||
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
local char1 = string.sub(one, -1)
|
||||||
}
|
local char2 = string.sub(two, 1, 1)
|
||||||
end
|
local nonAlphaNumeric1 = string.match(char1, "%W")
|
||||||
|
local nonAlphaNumeric2 = string.match(char2, "%W")
|
||||||
|
local whitespace1 = nonAlphaNumeric1 and string.match(char1, "%s")
|
||||||
|
local whitespace2 = nonAlphaNumeric2 and string.match(char2, "%s")
|
||||||
|
local lineBreak1 = whitespace1 and string.match(char1, "%c")
|
||||||
|
local lineBreak2 = whitespace2 and string.match(char2, "%c")
|
||||||
|
local blankLine1 = lineBreak1 and string.match(one, "\n\r?\n$")
|
||||||
|
local blankLine2 = lineBreak2 and string.match(two, "^\r?\n\r?\n")
|
||||||
|
|
||||||
return StringDiff._bisect(text1, text2)
|
if blankLine1 or blankLine2 then
|
||||||
|
-- Five points for blank lines.
|
||||||
|
return 5
|
||||||
|
elseif lineBreak1 or lineBreak2 then
|
||||||
|
-- Four points for line breaks
|
||||||
|
-- DEVIATION: Prefer to start on a line break instead of end on it
|
||||||
|
return if lineBreak1 then 4 else 4.5
|
||||||
|
elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then
|
||||||
|
-- Three points for end of sentences.
|
||||||
|
return 3
|
||||||
|
elseif whitespace1 or whitespace2 then
|
||||||
|
-- Two points for whitespace.
|
||||||
|
return 2
|
||||||
|
elseif nonAlphaNumeric1 or nonAlphaNumeric2 then
|
||||||
|
-- One point for non-alphanumeric.
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._cleanupSemanticLossless(diffs: Diffs)
|
||||||
|
-- Look for single edits surrounded on both sides by equalities
|
||||||
|
-- which can be shifted sideways to align the edit to a word boundary.
|
||||||
|
-- e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
|
||||||
|
|
||||||
|
local pointer = 2
|
||||||
|
-- Intentionally ignore the first and last element (don't need checking).
|
||||||
|
while diffs[pointer + 1] do
|
||||||
|
local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]
|
||||||
|
if
|
||||||
|
(prevDiff.actionType == StringDiff.ActionTypes.Equal)
|
||||||
|
and (nextDiff.actionType == StringDiff.ActionTypes.Equal)
|
||||||
|
then
|
||||||
|
-- This is a single edit surrounded by equalities.
|
||||||
|
local diff = diffs[pointer]
|
||||||
|
|
||||||
|
local equality1 = prevDiff.value
|
||||||
|
local edit = diff.value
|
||||||
|
local equality2 = nextDiff.value
|
||||||
|
|
||||||
|
-- First, shift the edit as far left as possible.
|
||||||
|
local commonOffset = StringDiff._sharedSuffix(equality1, edit)
|
||||||
|
if commonOffset > 0 then
|
||||||
|
local commonString = string.sub(edit, -commonOffset)
|
||||||
|
equality1 = string.sub(equality1, 1, -commonOffset - 1)
|
||||||
|
edit = commonString .. string.sub(edit, 1, -commonOffset - 1)
|
||||||
|
equality2 = commonString .. equality2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Second, step character by character right, looking for the best fit.
|
||||||
|
local bestEquality1 = equality1
|
||||||
|
local bestEdit = edit
|
||||||
|
local bestEquality2 = equality2
|
||||||
|
local bestScore = StringDiff._cleanupSemanticScore(equality1, edit)
|
||||||
|
+ StringDiff._cleanupSemanticScore(edit, equality2)
|
||||||
|
|
||||||
|
while string.byte(edit, 1) == string.byte(equality2, 1) do
|
||||||
|
equality1 = equality1 .. string.sub(edit, 1, 1)
|
||||||
|
edit = string.sub(edit, 2) .. string.sub(equality2, 1, 1)
|
||||||
|
equality2 = string.sub(equality2, 2)
|
||||||
|
local score = StringDiff._cleanupSemanticScore(equality1, edit)
|
||||||
|
+ StringDiff._cleanupSemanticScore(edit, equality2)
|
||||||
|
-- The > (rather than >=) encourages leading rather than trailing whitespace on edits.
|
||||||
|
-- I just think it looks better for indentation changes to start the line,
|
||||||
|
-- since then indenting several lines all have aligned diffs at the start
|
||||||
|
if score > bestScore then
|
||||||
|
bestScore = score
|
||||||
|
bestEquality1 = equality1
|
||||||
|
bestEdit = edit
|
||||||
|
bestEquality2 = equality2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if prevDiff.value ~= bestEquality1 then
|
||||||
|
-- We have an improvement, save it back to the diff.
|
||||||
|
if #bestEquality1 > 0 then
|
||||||
|
diffs[pointer - 1].value = bestEquality1
|
||||||
|
else
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
pointer = pointer - 1
|
||||||
|
end
|
||||||
|
diffs[pointer].value = bestEdit
|
||||||
|
if #bestEquality2 > 0 then
|
||||||
|
diffs[pointer + 1].value = bestEquality2
|
||||||
|
else
|
||||||
|
table.remove(diffs, pointer + 1)
|
||||||
|
pointer = pointer - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiff._bisect(text1: string, text2: string): Diffs
|
function StringDiff._bisect(text1: string, text2: string): Diffs
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -7,14 +5,15 @@ local Packages = Rojo.Packages
|
|||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
local Highlighter = require(Packages.Highlighter)
|
local Highlighter = require(Packages.Highlighter)
|
||||||
|
Highlighter.matchStudioSettings()
|
||||||
local StringDiff = require(script:FindFirstChild("StringDiff"))
|
local StringDiff = require(script:FindFirstChild("StringDiff"))
|
||||||
|
|
||||||
local Timer = require(Plugin.Timer)
|
local Timer = require(Plugin.Timer)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -22,27 +21,29 @@ local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer")
|
|||||||
|
|
||||||
function StringDiffVisualizer:init()
|
function StringDiffVisualizer:init()
|
||||||
self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0))
|
self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0))
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
self.updateEvent = Instance.new("BindableEvent")
|
||||||
|
self.lineHeight, self.setLineHeight = Roact.createBinding(15)
|
||||||
|
self.canvasPosition, self.setCanvasPosition = Roact.createBinding(Vector2.zero)
|
||||||
|
self.windowWidth, self.setWindowWidth = Roact.createBinding(math.huge)
|
||||||
|
|
||||||
-- Ensure that the script background is up to date with the current theme
|
-- Ensure that the script background is up to date with the current theme
|
||||||
self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function()
|
self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function()
|
||||||
task.defer(function()
|
-- Delay to allow Highlighter to process the theme change first
|
||||||
-- Defer to allow Highlighter to process the theme change first
|
task.delay(1 / 20, function()
|
||||||
self:updateScriptBackground()
|
self:updateScriptBackground()
|
||||||
|
self:updateDiffs()
|
||||||
|
-- Rerender the virtual list elements
|
||||||
|
self.updateEvent:Fire()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
self:calculateContentSize()
|
|
||||||
self:updateScriptBackground()
|
self:updateScriptBackground()
|
||||||
|
self:updateDiffs()
|
||||||
self:setState({
|
|
||||||
add = {},
|
|
||||||
remove = {},
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiffVisualizer:willUnmount()
|
function StringDiffVisualizer:willUnmount()
|
||||||
self.themeChangedConnection:Disconnect()
|
self.themeChangedConnection:Disconnect()
|
||||||
|
self.updateEvent:Destroy()
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiffVisualizer:updateScriptBackground()
|
function StringDiffVisualizer:updateScriptBackground()
|
||||||
@@ -53,96 +54,189 @@ function StringDiffVisualizer:updateScriptBackground()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function StringDiffVisualizer:didUpdate(previousProps)
|
function StringDiffVisualizer:didUpdate(previousProps)
|
||||||
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
if
|
||||||
self:calculateContentSize()
|
previousProps.currentString ~= self.props.currentString
|
||||||
local add, remove = self:calculateDiffLines()
|
or previousProps.incomingString ~= self.props.incomingString
|
||||||
self:setState({
|
then
|
||||||
add = add,
|
self:updateDiffs()
|
||||||
remove = remove,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiffVisualizer:calculateContentSize()
|
function StringDiffVisualizer:updateDiffs()
|
||||||
local oldString, newString = self.props.oldString, self.props.newString
|
Timer.start("StringDiffVisualizer:updateDiffs")
|
||||||
|
local currentString, incomingString = self.props.currentString, self.props.incomingString
|
||||||
local oldStringBounds = TextService:GetTextSize(oldString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
|
||||||
local newStringBounds = TextService:GetTextSize(newString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
|
||||||
|
|
||||||
self.setContentSize(
|
|
||||||
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y))
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
function StringDiffVisualizer:calculateDiffLines()
|
|
||||||
Timer.start("StringDiffVisualizer:calculateDiffLines")
|
|
||||||
local oldString, newString = self.props.oldString, self.props.newString
|
|
||||||
|
|
||||||
-- Diff the two texts
|
-- Diff the two texts
|
||||||
local startClock = os.clock()
|
local startClock = os.clock()
|
||||||
local diffs = StringDiff.findDiffs(oldString, newString)
|
local diffs =
|
||||||
|
StringDiff.findDiffs((string.gsub(currentString, "\t", " ")), (string.gsub(incomingString, "\t", " ")))
|
||||||
local stopClock = os.clock()
|
local stopClock = os.clock()
|
||||||
|
|
||||||
Log.trace(
|
Log.trace(
|
||||||
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||||
#oldString,
|
#currentString,
|
||||||
#newString,
|
#incomingString,
|
||||||
math.round((stopClock - startClock) * 1000 * 1000),
|
math.round((stopClock - startClock) * 1000 * 1000),
|
||||||
#diffs
|
#diffs
|
||||||
)
|
)
|
||||||
|
|
||||||
-- Determine which lines to highlight
|
-- Build the rich text lines
|
||||||
local add, remove = {}, {}
|
local currentRichTextLines = Highlighter.buildRichTextLines({
|
||||||
|
src = currentString,
|
||||||
|
})
|
||||||
|
local incomingRichTextLines = Highlighter.buildRichTextLines({
|
||||||
|
src = incomingString,
|
||||||
|
})
|
||||||
|
|
||||||
local oldLineNum, newLineNum = 1, 1
|
local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines)
|
||||||
|
|
||||||
|
-- Find the diff locations
|
||||||
|
local currentDiffs, incomingDiffs = {}, {}
|
||||||
|
local firstDiffLineNum = 0
|
||||||
|
|
||||||
|
local currentLineNum, incomingLineNum = 1, 1
|
||||||
|
local currentIdx, incomingIdx = 1, 1
|
||||||
for _, diff in diffs do
|
for _, diff in diffs do
|
||||||
local actionType, text = diff.actionType, diff.value
|
local actionType, text = diff.actionType, diff.value
|
||||||
local lines = select(2, string.gsub(text, "\n", "\n"))
|
local lineCount = select(2, string.gsub(text, "\n", "\n"))
|
||||||
|
local lines = string.split(text, "\n")
|
||||||
|
|
||||||
if actionType == StringDiff.ActionTypes.Equal then
|
if actionType == StringDiff.ActionTypes.Equal then
|
||||||
oldLineNum += lines
|
if lineCount > 0 then
|
||||||
newLineNum += lines
|
-- Jump cursor ahead to last line
|
||||||
elseif actionType == StringDiff.ActionTypes.Insert then
|
currentLineNum += lineCount
|
||||||
if lines > 0 then
|
incomingLineNum += lineCount
|
||||||
local textLines = string.split(text, "\n")
|
currentIdx = #lines[#lines]
|
||||||
for i, textLine in textLines do
|
incomingIdx = #lines[#lines]
|
||||||
if string.match(textLine, "%S") then
|
|
||||||
add[newLineNum + i - 1] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
if string.match(text, "%S") then
|
-- Move along this line
|
||||||
add[newLineNum] = true
|
currentIdx += #text
|
||||||
end
|
incomingIdx += #text
|
||||||
|
end
|
||||||
|
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if actionType == StringDiff.ActionTypes.Insert then
|
||||||
|
if firstDiffLineNum == 0 then
|
||||||
|
firstDiffLineNum = incomingLineNum
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, lineText in lines do
|
||||||
|
if i > 1 then
|
||||||
|
-- Move to next line
|
||||||
|
incomingLineNum += 1
|
||||||
|
incomingIdx = 0
|
||||||
|
end
|
||||||
|
if not incomingDiffs[incomingLineNum] then
|
||||||
|
incomingDiffs[incomingLineNum] = {}
|
||||||
|
end
|
||||||
|
-- Mark these characters on this line
|
||||||
|
table.insert(incomingDiffs[incomingLineNum], {
|
||||||
|
start = incomingIdx,
|
||||||
|
stop = incomingIdx + #lineText,
|
||||||
|
})
|
||||||
|
incomingIdx += #lineText
|
||||||
end
|
end
|
||||||
newLineNum += lines
|
|
||||||
elseif actionType == StringDiff.ActionTypes.Delete then
|
elseif actionType == StringDiff.ActionTypes.Delete then
|
||||||
if lines > 0 then
|
if firstDiffLineNum == 0 then
|
||||||
local textLines = string.split(text, "\n")
|
firstDiffLineNum = currentLineNum
|
||||||
for i, textLine in textLines do
|
end
|
||||||
if string.match(textLine, "%S") then
|
|
||||||
remove[oldLineNum + i - 1] = true
|
for i, lineText in lines do
|
||||||
end
|
if i > 1 then
|
||||||
end
|
-- Move to next line
|
||||||
else
|
currentLineNum += 1
|
||||||
if string.match(text, "%S") then
|
currentIdx = 0
|
||||||
remove[oldLineNum] = true
|
end
|
||||||
end
|
if not currentDiffs[currentLineNum] then
|
||||||
|
currentDiffs[currentLineNum] = {}
|
||||||
|
end
|
||||||
|
-- Mark these characters on this line
|
||||||
|
table.insert(currentDiffs[currentLineNum], {
|
||||||
|
start = currentIdx,
|
||||||
|
stop = currentIdx + #lineText,
|
||||||
|
})
|
||||||
|
currentIdx += #lineText
|
||||||
end
|
end
|
||||||
oldLineNum += lines
|
|
||||||
else
|
else
|
||||||
Log.warn("Unknown diff action: {} {}", actionType, text)
|
Log.warn("Unknown diff action: {} {}", actionType, text)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Timer.stop()
|
Timer.stop()
|
||||||
return add, remove
|
|
||||||
|
self:setState({
|
||||||
|
maxLines = maxLines,
|
||||||
|
currentRichTextLines = currentRichTextLines,
|
||||||
|
incomingRichTextLines = incomingRichTextLines,
|
||||||
|
currentDiffs = currentDiffs,
|
||||||
|
incomingDiffs = incomingDiffs,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Scroll to the first diff line
|
||||||
|
task.defer(self.setCanvasPosition, Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16)))
|
||||||
end
|
end
|
||||||
|
|
||||||
function StringDiffVisualizer:render()
|
function StringDiffVisualizer:render()
|
||||||
local oldString, newString = self.props.oldString, self.props.newString
|
local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs
|
||||||
|
local currentRichTextLines, incomingRichTextLines =
|
||||||
|
self.state.currentRichTextLines, self.state.incomingRichTextLines
|
||||||
|
local maxLines = self.state.maxLines
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
|
self.setLineHeight(theme.TextSize.Code)
|
||||||
|
|
||||||
|
-- Calculate the width of the canvas
|
||||||
|
-- (One line at a time to avoid the char limit of getTextBoundsAsync)
|
||||||
|
local canvasWidth = 0
|
||||||
|
for i = 1, maxLines do
|
||||||
|
local currentLine = currentRichTextLines[i]
|
||||||
|
if currentLine and string.find(currentLine, "%S") then
|
||||||
|
local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true)
|
||||||
|
if bounds.X > canvasWidth then
|
||||||
|
canvasWidth = bounds.X
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local incomingLine = incomingRichTextLines[i]
|
||||||
|
if incomingLine and string.find(incomingLine, "%S") then
|
||||||
|
local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true)
|
||||||
|
if bounds.X > canvasWidth then
|
||||||
|
canvasWidth = bounds.X
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local lineNumberWidth =
|
||||||
|
getTextBoundsAsync(tostring(maxLines), theme.Font.Code, theme.TextSize.Body, math.huge, true).X
|
||||||
|
|
||||||
|
canvasWidth += lineNumberWidth + 12
|
||||||
|
|
||||||
|
local removalScrollMarkers = {}
|
||||||
|
local insertionScrollMarkers = {}
|
||||||
|
for lineNum in currentDiffs do
|
||||||
|
table.insert(
|
||||||
|
removalScrollMarkers,
|
||||||
|
e("Frame", {
|
||||||
|
Size = UDim2.fromScale(0.5, 1 / maxLines),
|
||||||
|
Position = UDim2.fromScale(0, (lineNum - 1) / maxLines),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Remove,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
for lineNum in incomingDiffs do
|
||||||
|
table.insert(
|
||||||
|
insertionScrollMarkers,
|
||||||
|
e("Frame", {
|
||||||
|
Size = UDim2.fromScale(0.5, 1 / maxLines),
|
||||||
|
Position = UDim2.fromScale(0.5, (lineNum - 1) / maxLines),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Add,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
size = self.props.size,
|
size = self.props.size,
|
||||||
position = self.props.position,
|
position = self.props.position,
|
||||||
@@ -160,43 +254,196 @@ function StringDiffVisualizer:render()
|
|||||||
CornerRadius = UDim.new(0, 5),
|
CornerRadius = UDim.new(0, 5),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
Separator = e("Frame", {
|
Main = e("Frame", {
|
||||||
Size = UDim2.new(0, 2, 1, 0),
|
Size = UDim2.new(1, -10, 1, -2),
|
||||||
Position = UDim2.new(0.5, 0, 0, 0),
|
Position = UDim2.new(0, 2, 0, 2),
|
||||||
AnchorPoint = Vector2.new(0.5, 0),
|
BackgroundTransparency = 1,
|
||||||
BorderSizePixel = 0,
|
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||||
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
self.setWindowWidth(rbx.AbsoluteSize.X * 0.5 - 10)
|
||||||
BackgroundTransparency = 0.5,
|
end,
|
||||||
}),
|
|
||||||
Old = e(ScrollingFrame, {
|
|
||||||
position = UDim2.new(0, 2, 0, 2),
|
|
||||||
size = UDim2.new(0.5, -7, 1, -4),
|
|
||||||
scrollingDirection = Enum.ScrollingDirection.XY,
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
contentSize = self.contentSize,
|
|
||||||
}, {
|
}, {
|
||||||
Source = e(CodeLabel, {
|
Separator = e("Frame", {
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(0, 2, 1, 0),
|
||||||
|
Position = UDim2.new(0.5, 0, 0, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
BackgroundTransparency = 0.5,
|
||||||
|
}),
|
||||||
|
Current = e(VirtualScroller, {
|
||||||
position = UDim2.new(0, 0, 0, 0),
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
text = oldString,
|
size = UDim2.new(0.5, -1, 1, 0),
|
||||||
lineBackground = theme.Diff.Remove,
|
transparency = self.props.transparency,
|
||||||
markedLines = self.state.remove,
|
count = maxLines,
|
||||||
|
updateEvent = self.updateEvent.Event,
|
||||||
|
canvasWidth = canvasWidth,
|
||||||
|
canvasPosition = self.canvasPosition,
|
||||||
|
onCanvasPositionChanged = self.setCanvasPosition,
|
||||||
|
render = function(i)
|
||||||
|
local lineDiffs = currentDiffs[i]
|
||||||
|
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
|
||||||
|
|
||||||
|
-- Show diff markers over the specific changed characters
|
||||||
|
if lineDiffs then
|
||||||
|
local charWidth = math.round(theme.TextSize.Code * 0.5)
|
||||||
|
for diffIdx, diff in lineDiffs do
|
||||||
|
local start, stop = diff.start, diff.stop
|
||||||
|
diffFrames[diffIdx] = e("Frame", {
|
||||||
|
Size = if #lineDiffs == 1
|
||||||
|
and start == 0
|
||||||
|
and stop == 0
|
||||||
|
then UDim2.fromScale(1, 1)
|
||||||
|
else UDim2.new(
|
||||||
|
0,
|
||||||
|
math.max(charWidth * (stop - start), charWidth * 0.4),
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
Position = UDim2.fromOffset(charWidth * start, 0),
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Remove,
|
||||||
|
BackgroundTransparency = 0.85,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
ZIndex = -1,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment({
|
||||||
|
LineNumber = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
|
||||||
|
Text = i,
|
||||||
|
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||||
|
BackgroundTransparency = 0.9,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
FontFace = theme.Font.Code,
|
||||||
|
TextSize = theme.TextSize.Body,
|
||||||
|
TextColor3 = if lineDiffs then theme.Diff.Background.Remove else theme.SubTextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }),
|
||||||
|
}),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0),
|
||||||
|
Position = UDim2.fromScale(1, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Remove,
|
||||||
|
BackgroundTransparency = if lineDiffs then 0.95 else 1,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
}, {
|
||||||
|
CodeLabel = e("TextLabel", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
Position = UDim2.fromScale(0, 0),
|
||||||
|
Text = currentRichTextLines[i] or "",
|
||||||
|
RichText = true,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
FontFace = theme.Font.Code,
|
||||||
|
TextSize = theme.TextSize.Code,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
|
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||||
|
}),
|
||||||
|
DiffFrames = Roact.createFragment(diffFrames),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
getHeightBinding = function()
|
||||||
|
return self.lineHeight
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
Incoming = e(VirtualScroller, {
|
||||||
|
position = UDim2.new(0.5, 1, 0, 0),
|
||||||
|
size = UDim2.new(0.5, -1, 1, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
count = maxLines,
|
||||||
|
updateEvent = self.updateEvent.Event,
|
||||||
|
canvasWidth = canvasWidth,
|
||||||
|
canvasPosition = self.canvasPosition,
|
||||||
|
onCanvasPositionChanged = self.setCanvasPosition,
|
||||||
|
render = function(i)
|
||||||
|
local lineDiffs = incomingDiffs[i]
|
||||||
|
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
|
||||||
|
|
||||||
|
-- Show diff markers over the specific changed characters
|
||||||
|
if lineDiffs then
|
||||||
|
local charWidth = math.round(theme.TextSize.Code * 0.5)
|
||||||
|
for diffIdx, diff in lineDiffs do
|
||||||
|
local start, stop = diff.start, diff.stop
|
||||||
|
diffFrames[diffIdx] = e("Frame", {
|
||||||
|
Size = if #lineDiffs == 1
|
||||||
|
and start == 0
|
||||||
|
and stop == 0
|
||||||
|
then UDim2.fromScale(1, 1)
|
||||||
|
else UDim2.new(
|
||||||
|
0,
|
||||||
|
math.max(charWidth * (stop - start), charWidth * 0.4),
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
Position = UDim2.fromOffset(charWidth * start, 0),
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Add,
|
||||||
|
BackgroundTransparency = 0.85,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
ZIndex = -1,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment({
|
||||||
|
LineNumber = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
|
||||||
|
Text = i,
|
||||||
|
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||||
|
BackgroundTransparency = 0.9,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
FontFace = theme.Font.Code,
|
||||||
|
TextSize = theme.TextSize.Body,
|
||||||
|
TextColor3 = if lineDiffs then theme.Diff.Background.Add else theme.SubTextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }),
|
||||||
|
}),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0),
|
||||||
|
Position = UDim2.fromScale(1, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Add,
|
||||||
|
BackgroundTransparency = if lineDiffs then 0.95 else 1,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
}, {
|
||||||
|
CodeLabel = e("TextLabel", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
Position = UDim2.fromScale(0, 0),
|
||||||
|
Text = incomingRichTextLines[i] or "",
|
||||||
|
RichText = true,
|
||||||
|
BackgroundColor3 = theme.Diff.Background.Add,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
FontFace = theme.Font.Code,
|
||||||
|
TextSize = theme.TextSize.Code,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
|
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||||
|
}),
|
||||||
|
DiffFrames = Roact.createFragment(diffFrames),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
getHeightBinding = function()
|
||||||
|
return self.lineHeight
|
||||||
|
end,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
New = e(ScrollingFrame, {
|
ScrollMarkers = e("Frame", {
|
||||||
position = UDim2.new(0.5, 5, 0, 2),
|
Size = self.windowWidth:map(function(windowWidth)
|
||||||
size = UDim2.new(0.5, -7, 1, -4),
|
return UDim2.new(0, 8, 1, -4 - (if canvasWidth > windowWidth then 10 else 0))
|
||||||
scrollingDirection = Enum.ScrollingDirection.XY,
|
end),
|
||||||
transparency = self.props.transparency,
|
Position = UDim2.new(1, -2, 0, 2),
|
||||||
contentSize = self.contentSize,
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Source = e(CodeLabel, {
|
insertions = Roact.createFragment(insertionScrollMarkers),
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
removals = Roact.createFragment(removalScrollMarkers),
|
||||||
position = UDim2.new(0, 0, 0, 0),
|
|
||||||
text = newString,
|
|
||||||
lineBackground = theme.Diff.Add,
|
|
||||||
markedLines = self.state.add,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ function Array:render()
|
|||||||
e("Frame", {
|
e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 25),
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
|
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
|
||||||
BackgroundColor3 = if patchType == "Remain" then theme.Diff.Row else theme.Diff[patchType],
|
BackgroundColor3 = theme.Diff.Background[patchType],
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
LayoutOrder = i,
|
LayoutOrder = i,
|
||||||
}, {
|
}, {
|
||||||
@@ -152,8 +152,8 @@ function Array:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "Old",
|
Text = "Old",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
@@ -163,8 +163,8 @@ function Array:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "New",
|
Text = "New",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ function Dictionary:render()
|
|||||||
LayoutOrder = order,
|
LayoutOrder = order,
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
||||||
BackgroundColor3 = if line.patchType == "Remain"
|
BackgroundColor3 = theme.Diff.Background[line.patchType],
|
||||||
then theme.Diff.Row
|
|
||||||
else theme.Diff[line.patchType],
|
|
||||||
}, {
|
}, {
|
||||||
DiffIcon = if line.patchType ~= "Remain"
|
DiffIcon = if line.patchType ~= "Remain"
|
||||||
then e("ImageLabel", {
|
then e("ImageLabel", {
|
||||||
@@ -112,9 +110,9 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = key,
|
Text = key,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Diff.Text[line.patchType],
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
OldValue = e("Frame", {
|
OldValue = e("Frame", {
|
||||||
@@ -125,7 +123,7 @@ function Dictionary:render()
|
|||||||
e(DisplayValue, {
|
e(DisplayValue, {
|
||||||
value = oldValue,
|
value = oldValue,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
textColor = theme.Settings.Setting.DescriptionColor,
|
textColor = theme.Diff.Text[line.patchType],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
NewValue = e("Frame", {
|
NewValue = e("Frame", {
|
||||||
@@ -136,7 +134,7 @@ function Dictionary:render()
|
|||||||
e(DisplayValue, {
|
e(DisplayValue, {
|
||||||
value = newValue,
|
value = newValue,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
textColor = theme.Settings.Setting.DescriptionColor,
|
textColor = theme.Diff.Text[line.patchType],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -157,8 +155,8 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "Key",
|
Text = "Key",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
@@ -168,8 +166,8 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "Old",
|
Text = "Old",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
@@ -179,8 +177,8 @@ function Dictionary:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "New",
|
Text = "New",
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||||
@@ -11,46 +12,48 @@ local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
|||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
return function(props)
|
return function(props)
|
||||||
return e(SlicedImage, {
|
return Theme.with(function(theme)
|
||||||
slice = Assets.Slices.RoundedBackground,
|
return e(SlicedImage, {
|
||||||
color = props.color,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
transparency = props.transparency:map(function(transparency)
|
color = props.color,
|
||||||
return 0.9 + (0.1 * transparency)
|
transparency = props.transparency:map(function(transparency)
|
||||||
end),
|
return 0.9 + (0.1 * transparency)
|
||||||
layoutOrder = props.layoutOrder,
|
end),
|
||||||
position = props.position,
|
layoutOrder = props.layoutOrder,
|
||||||
anchorPoint = props.anchorPoint,
|
position = props.position,
|
||||||
size = UDim2.new(0, 0, 0, 16),
|
anchorPoint = props.anchorPoint,
|
||||||
automaticSize = Enum.AutomaticSize.X,
|
size = UDim2.new(0, 0, 0, theme.TextSize.Medium),
|
||||||
}, {
|
automaticSize = Enum.AutomaticSize.X,
|
||||||
Padding = e("UIPadding", {
|
}, {
|
||||||
PaddingLeft = UDim.new(0, 4),
|
Padding = e("UIPadding", {
|
||||||
PaddingRight = UDim.new(0, 4),
|
PaddingLeft = UDim.new(0, 4),
|
||||||
PaddingTop = UDim.new(0, 2),
|
PaddingRight = UDim.new(0, 4),
|
||||||
PaddingBottom = UDim.new(0, 2),
|
PaddingTop = UDim.new(0, 2),
|
||||||
}),
|
PaddingBottom = UDim.new(0, 2),
|
||||||
Icon = if props.icon
|
}),
|
||||||
then e("ImageLabel", {
|
Icon = if props.icon
|
||||||
Size = UDim2.new(0, 12, 0, 12),
|
then e("ImageLabel", {
|
||||||
Position = UDim2.new(0, 0, 0.5, 0),
|
Size = UDim2.new(0, 12, 0, 12),
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
Position = UDim2.new(0, 0, 0.5, 0),
|
||||||
Image = props.icon,
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
Image = props.icon,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
ImageColor3 = props.color,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
Text = e("TextLabel", {
|
||||||
|
Text = props.text,
|
||||||
|
FontFace = theme.Font.Main,
|
||||||
|
TextSize = theme.TextSize.Small,
|
||||||
|
TextColor3 = props.color,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
ImageColor3 = props.color,
|
}),
|
||||||
ImageTransparency = props.transparency,
|
})
|
||||||
})
|
end)
|
||||||
else nil,
|
|
||||||
Text = e("TextLabel", {
|
|
||||||
Text = props.text,
|
|
||||||
Font = Enum.Font.GothamMedium,
|
|
||||||
TextSize = 12,
|
|
||||||
TextColor3 = props.color,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Center,
|
|
||||||
TextTransparency = props.transparency,
|
|
||||||
Size = UDim2.new(0, 0, 1, 0),
|
|
||||||
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
|
|
||||||
AutomaticSize = Enum.AutomaticSize.X,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -10,6 +8,7 @@ local Flipper = require(Packages.Flipper)
|
|||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local SlicedImage = require(script.Parent.SlicedImage)
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
local TouchRipple = require(script.Parent.TouchRipple)
|
local TouchRipple = require(script.Parent.TouchRipple)
|
||||||
@@ -41,18 +40,17 @@ end
|
|||||||
|
|
||||||
function TextButton:render()
|
function TextButton:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local textSize =
|
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Large, math.huge)
|
||||||
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
|
|
||||||
|
|
||||||
local style = self.props.style
|
local style = self.props.style
|
||||||
|
|
||||||
theme = theme.Button[style]
|
local buttonTheme = theme.Button[style]
|
||||||
|
|
||||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||||
|
|
||||||
return e("ImageButton", {
|
return e("ImageButton", {
|
||||||
Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34),
|
Size = UDim2.new(0, (theme.TextSize.Body * 2) + textBounds.X, 0, 34),
|
||||||
Position = self.props.position,
|
Position = self.props.position,
|
||||||
AnchorPoint = self.props.anchorPoint,
|
AnchorPoint = self.props.anchorPoint,
|
||||||
|
|
||||||
@@ -74,18 +72,22 @@ function TextButton:render()
|
|||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
TouchRipple = e(TouchRipple, {
|
TouchRipple = e(TouchRipple, {
|
||||||
color = theme.ActionFillColor,
|
color = buttonTheme.ActionFillColor,
|
||||||
transparency = self.props.transparency:map(function(value)
|
transparency = self.props.transparency:map(function(value)
|
||||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
|
return bindingUtil.blendAlpha({ buttonTheme.ActionFillTransparency, value })
|
||||||
end),
|
end),
|
||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
TextColor3 = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
buttonTheme.Enabled.TextColor,
|
||||||
|
buttonTheme.Disabled.TextColor
|
||||||
|
),
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -95,7 +97,11 @@ function TextButton:render()
|
|||||||
|
|
||||||
Border = style == "Bordered" and e(SlicedImage, {
|
Border = style == "Bordered" and e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
color = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
buttonTheme.Enabled.BorderColor,
|
||||||
|
buttonTheme.Disabled.BorderColor
|
||||||
|
),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -105,14 +111,18 @@ function TextButton:render()
|
|||||||
|
|
||||||
HoverOverlay = e(SlicedImage, {
|
HoverOverlay = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.ActionFillColor,
|
color = buttonTheme.ActionFillColor,
|
||||||
transparency = Roact.joinBindings({
|
transparency = Roact.joinBindings({
|
||||||
hover = bindingHover:map(function(value)
|
hover = bindingHover:map(function(value)
|
||||||
return 1 - value
|
return 1 - value
|
||||||
end),
|
end),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
}):map(function(values)
|
}):map(function(values)
|
||||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
return bindingUtil.blendAlpha({
|
||||||
|
buttonTheme.ActionFillTransparency,
|
||||||
|
values.hover,
|
||||||
|
values.transparency,
|
||||||
|
})
|
||||||
end),
|
end),
|
||||||
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -124,8 +134,8 @@ function TextButton:render()
|
|||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = bindingUtil.mapLerp(
|
color = bindingUtil.mapLerp(
|
||||||
bindingEnabled,
|
bindingEnabled,
|
||||||
theme.Enabled.BackgroundColor,
|
buttonTheme.Enabled.BackgroundColor,
|
||||||
theme.Disabled.BackgroundColor
|
buttonTheme.Disabled.BackgroundColor
|
||||||
),
|
),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,18 @@ end
|
|||||||
|
|
||||||
function TextInput:render()
|
function TextInput:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.TextInput
|
local textInputTheme = theme.TextInput
|
||||||
|
|
||||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||||
|
|
||||||
return e(SlicedImage, {
|
return e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBorder,
|
slice = Assets.Slices.RoundedBorder,
|
||||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
color = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
textInputTheme.Enabled.BorderColor,
|
||||||
|
textInputTheme.Disabled.BorderColor
|
||||||
|
),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
||||||
@@ -55,14 +59,18 @@ function TextInput:render()
|
|||||||
}, {
|
}, {
|
||||||
HoverOverlay = e(SlicedImage, {
|
HoverOverlay = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.ActionFillColor,
|
color = textInputTheme.ActionFillColor,
|
||||||
transparency = Roact.joinBindings({
|
transparency = Roact.joinBindings({
|
||||||
hover = bindingHover:map(function(value)
|
hover = bindingHover:map(function(value)
|
||||||
return 1 - value
|
return 1 - value
|
||||||
end),
|
end),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
}):map(function(values)
|
}):map(function(values)
|
||||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
return bindingUtil.blendAlpha({
|
||||||
|
textInputTheme.ActionFillTransparency,
|
||||||
|
values.hover,
|
||||||
|
values.transparency,
|
||||||
|
})
|
||||||
end),
|
end),
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
zIndex = -1,
|
zIndex = -1,
|
||||||
@@ -72,14 +80,18 @@ function TextInput:render()
|
|||||||
Size = UDim2.fromScale(1, 1),
|
Size = UDim2.fromScale(1, 1),
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
PlaceholderText = self.props.placeholder,
|
PlaceholderText = self.props.placeholder,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
|
TextColor3 = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
textInputTheme.Disabled.TextColor,
|
||||||
|
textInputTheme.Enabled.TextColor
|
||||||
|
),
|
||||||
PlaceholderColor3 = bindingUtil.mapLerp(
|
PlaceholderColor3 = bindingUtil.mapLerp(
|
||||||
bindingEnabled,
|
bindingEnabled,
|
||||||
theme.Disabled.PlaceholderColor,
|
textInputTheme.Disabled.PlaceholderColor,
|
||||||
theme.Enabled.PlaceholderColor
|
textInputTheme.Enabled.PlaceholderColor
|
||||||
),
|
),
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextEditable = self.props.enabled,
|
TextEditable = self.props.enabled,
|
||||||
ClearTextOnFocus = self.props.clearTextOnFocus,
|
ClearTextOnFocus = self.props.clearTextOnFocus,
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
@@ -8,6 +7,8 @@ local Packages = Rojo.Packages
|
|||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
@@ -21,50 +22,48 @@ local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to
|
|||||||
local TooltipContext = Roact.createContext({})
|
local TooltipContext = Roact.createContext({})
|
||||||
|
|
||||||
local function Popup(props)
|
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 Theme.with(function(theme)
|
||||||
|
local textXSpace = math.min(props.parentSize.X, 250) - TEXT_PADDING.X
|
||||||
|
local textBounds = getTextBoundsAsync(props.Text, theme.Font.Main, theme.TextSize.Medium, textXSpace)
|
||||||
|
local contentSize = textBounds + 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 < contentSize.Y and spaceAbove > spaceBelow
|
||||||
|
|
||||||
|
local X = math.clamp(props.Position.X - X_OFFSET, 0, math.max(props.parentSize.X - contentSize.X, 1))
|
||||||
|
local Y = 0
|
||||||
|
|
||||||
|
if displayAbove then
|
||||||
|
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - contentSize.Y + Y_OVERLAP, 0)
|
||||||
|
else
|
||||||
|
Y = math.min(
|
||||||
|
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||||
|
props.parentSize.Y - contentSize.Y
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
position = UDim2.fromOffset(X, Y),
|
position = UDim2.fromOffset(X, Y),
|
||||||
size = UDim2.fromOffset(textSize.X, textSize.Y),
|
size = UDim2.fromOffset(contentSize.X, contentSize.Y),
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
}, {
|
}, {
|
||||||
Label = e("TextLabel", {
|
Label = e("TextLabel", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Position = UDim2.fromScale(0.5, 0.5),
|
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),
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
Size = UDim2.fromOffset(textBounds.X, textBounds.Y),
|
||||||
Text = props.Text,
|
Text = props.Text,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Medium,
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Center,
|
||||||
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
}),
|
}),
|
||||||
@@ -72,8 +71,8 @@ local function Popup(props)
|
|||||||
Tail = e("ImageLabel", {
|
Tail = e("ImageLabel", {
|
||||||
ZIndex = 100,
|
ZIndex = 100,
|
||||||
Position = if displayAbove
|
Position = if displayAbove
|
||||||
then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
|
then UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 1, -1)
|
||||||
else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
|
else UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||||
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
||||||
AnchorPoint = Vector2.new(0.5, 0),
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
Rotation = if displayAbove then 180 else 0,
|
Rotation = if displayAbove then 180 else 0,
|
||||||
@@ -217,7 +216,7 @@ function Trigger:managePopup()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
self.showDelayThread = task.delay(DELAY, function()
|
self.showDelayThread = task.delay(self.props.delay or DELAY, function()
|
||||||
self.props.context.addTip(self.id, {
|
self.props.context.addTip(self.id, {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Position = self:getMousePos(),
|
Position = self:getMousePos(),
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ local VirtualScroller = Roact.Component:extend("VirtualScroller")
|
|||||||
function VirtualScroller:init()
|
function VirtualScroller:init()
|
||||||
self.scrollFrameRef = Roact.createRef()
|
self.scrollFrameRef = Roact.createRef()
|
||||||
self:setState({
|
self:setState({
|
||||||
WindowSize = Vector2.new(),
|
WindowSize = Vector2.zero,
|
||||||
CanvasPosition = Vector2.new(),
|
CanvasPosition = if self.props.canvasPosition
|
||||||
|
then self.props.canvasPosition:getValue() or Vector2.zero
|
||||||
|
else Vector2.zero,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0)
|
self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0)
|
||||||
@@ -41,6 +43,10 @@ function VirtualScroller:didMount()
|
|||||||
|
|
||||||
local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition")
|
local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition")
|
||||||
self.canvasPositionChanged = canvasPositionSignal:Connect(function()
|
self.canvasPositionChanged = canvasPositionSignal:Connect(function()
|
||||||
|
if self.props.onCanvasPositionChanged then
|
||||||
|
pcall(self.props.onCanvasPositionChanged, rbx.CanvasPosition)
|
||||||
|
end
|
||||||
|
|
||||||
if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then
|
if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then
|
||||||
self:setState({ CanvasPosition = rbx.CanvasPosition })
|
self:setState({ CanvasPosition = rbx.CanvasPosition })
|
||||||
self:refresh()
|
self:refresh()
|
||||||
@@ -134,8 +140,9 @@ function VirtualScroller:render()
|
|||||||
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
|
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
|
||||||
BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor,
|
BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor,
|
||||||
CanvasSize = self.totalCanvas:map(function(s)
|
CanvasSize = self.totalCanvas:map(function(s)
|
||||||
return UDim2.fromOffset(0, s)
|
return UDim2.fromOffset(props.canvasWidth or 0, s)
|
||||||
end),
|
end),
|
||||||
|
CanvasPosition = self.props.canvasPosition,
|
||||||
ScrollBarThickness = 9,
|
ScrollBarThickness = 9,
|
||||||
ScrollBarImageColor3 = theme.ScrollBarColor,
|
ScrollBarImageColor3 = theme.ScrollBarColor,
|
||||||
ScrollBarImageTransparency = props.transparency:map(function(value)
|
ScrollBarImageTransparency = props.transparency:map(function(value)
|
||||||
@@ -146,7 +153,7 @@ function VirtualScroller:render()
|
|||||||
BottomImage = Assets.Images.ScrollBar.Bottom,
|
BottomImage = Assets.Images.ScrollBar.Bottom,
|
||||||
|
|
||||||
ElasticBehavior = Enum.ElasticBehavior.Always,
|
ElasticBehavior = Enum.ElasticBehavior.Always,
|
||||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
ScrollingDirection = Enum.ScrollingDirection.XY,
|
||||||
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||||
[Roact.Ref] = self.scrollFrameRef,
|
[Roact.Ref] = self.scrollFrameRef,
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Timer = require(Plugin.Timer)
|
|
||||||
local PatchTree = require(Plugin.PatchTree)
|
|
||||||
local Settings = require(Plugin.Settings)
|
local Settings = require(Plugin.Settings)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
@@ -24,36 +22,13 @@ function ConfirmingPage:init()
|
|||||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
patchTree = nil,
|
|
||||||
showingStringDiff = false,
|
showingStringDiff = false,
|
||||||
oldString = "",
|
currentString = "",
|
||||||
newString = "",
|
incomingString = "",
|
||||||
showingTableDiff = false,
|
showingTableDiff = false,
|
||||||
oldTable = {},
|
oldTable = {},
|
||||||
newTable = {},
|
newTable = {},
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.props.confirmData and self.props.confirmData.patch and self.props.confirmData.instanceMap then
|
|
||||||
self:buildPatchTree()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ConfirmingPage:didUpdate(prevProps)
|
|
||||||
if prevProps.confirmData ~= self.props.confirmData then
|
|
||||||
self:buildPatchTree()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ConfirmingPage:buildPatchTree()
|
|
||||||
Timer.start("ConfirmingPage:buildPatchTree")
|
|
||||||
self:setState({
|
|
||||||
patchTree = PatchTree.build(
|
|
||||||
self.props.confirmData.patch,
|
|
||||||
self.props.confirmData.instanceMap,
|
|
||||||
{ "Property", "Current", "Incoming" }
|
|
||||||
),
|
|
||||||
})
|
|
||||||
Timer.stop()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConfirmingPage:render()
|
function ConfirmingPage:render()
|
||||||
@@ -64,13 +39,13 @@ function ConfirmingPage:render()
|
|||||||
"Sync changes for project '%s':",
|
"Sync changes for project '%s':",
|
||||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||||
),
|
),
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(1, 0, 0, 20),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Large + 2),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -79,13 +54,13 @@ function ConfirmingPage:render()
|
|||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
patchTree = self.state.patchTree,
|
patchTree = self.props.patchTree,
|
||||||
|
|
||||||
showStringDiff = function(oldString: string, newString: string)
|
showStringDiff = function(currentString: string, incomingString: string)
|
||||||
self:setState({
|
self:setState({
|
||||||
showingStringDiff = true,
|
showingStringDiff = true,
|
||||||
oldString = oldString,
|
currentString = currentString,
|
||||||
newString = newString,
|
incomingString = incomingString,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
||||||
@@ -192,8 +167,8 @@ function ConfirmingPage:render()
|
|||||||
anchorPoint = Vector2.new(0, 0),
|
anchorPoint = Vector2.new(0, 0),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
oldString = self.state.oldString,
|
currentString = self.state.currentString,
|
||||||
newString = self.state.newString,
|
incomingString = self.state.incomingString,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ function ChangesViewer:render()
|
|||||||
|
|
||||||
Title = e("TextLabel", {
|
Title = e("TextLabel", {
|
||||||
Text = "Sync",
|
Text = "Sync",
|
||||||
Font = Enum.Font.GothamMedium,
|
FontFace = theme.Font.Main,
|
||||||
TextSize = 17,
|
TextSize = theme.TextSize.Large,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(1, -40, 0, 20),
|
Size = UDim2.new(1, -40, 0, theme.TextSize.Large + 2),
|
||||||
Position = UDim2.new(0, 40, 0, 0),
|
Position = UDim2.new(0, 40, 0, 0),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
@@ -74,13 +74,13 @@ function ChangesViewer:render()
|
|||||||
Subtitle = e("TextLabel", {
|
Subtitle = e("TextLabel", {
|
||||||
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = theme.SubTextColor,
|
TextColor3 = theme.SubTextColor,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(1, -40, 0, 16),
|
Size = UDim2.new(1, -40, 0, theme.TextSize.Medium),
|
||||||
Position = UDim2.new(0, 40, 0, 20),
|
Position = UDim2.new(0, 40, 0, theme.TextSize.Large + 2),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -131,8 +131,8 @@ function ChangesViewer:render()
|
|||||||
}),
|
}),
|
||||||
AppliedText = e("TextLabel", {
|
AppliedText = e("TextLabel", {
|
||||||
Text = applied,
|
Text = applied,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(0, 0, 1, 0),
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
@@ -156,8 +156,8 @@ function ChangesViewer:render()
|
|||||||
}),
|
}),
|
||||||
UnappliedText = e("TextLabel", {
|
UnappliedText = e("TextLabel", {
|
||||||
Text = unapplied,
|
Text = unapplied,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = theme.Diff.Warning,
|
TextColor3 = theme.Diff.Warning,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(0, 0, 1, 0),
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
@@ -217,13 +217,13 @@ local function ConnectionDetails(props)
|
|||||||
}, {
|
}, {
|
||||||
ProjectName = e("TextLabel", {
|
ProjectName = e("TextLabel", {
|
||||||
Text = props.projectName,
|
Text = props.projectName,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 20,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
|
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 20),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Large),
|
||||||
|
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -231,13 +231,13 @@ local function ConnectionDetails(props)
|
|||||||
|
|
||||||
Address = e("TextLabel", {
|
Address = e("TextLabel", {
|
||||||
Text = props.address,
|
Text = props.address,
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = theme.ConnectionDetails.AddressColor,
|
TextColor3 = theme.ConnectionDetails.AddressColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 15),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||||
|
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -307,8 +307,8 @@ function ConnectedPage:init()
|
|||||||
renderChanges = false,
|
renderChanges = false,
|
||||||
hoveringChangeInfo = false,
|
hoveringChangeInfo = false,
|
||||||
showingStringDiff = false,
|
showingStringDiff = false,
|
||||||
oldString = "",
|
currentString = "",
|
||||||
newString = "",
|
incomingString = "",
|
||||||
})
|
})
|
||||||
|
|
||||||
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||||
@@ -410,8 +410,8 @@ function ConnectedPage:render()
|
|||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = self.changeInfoText,
|
Text = self.changeInfoText,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 15,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
TextXAlignment = Enum.TextXAlignment.Right,
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
@@ -511,11 +511,11 @@ function ConnectedPage:render()
|
|||||||
patchData = self.props.patchData,
|
patchData = self.props.patchData,
|
||||||
patchTree = self.props.patchTree,
|
patchTree = self.props.patchTree,
|
||||||
serveSession = self.props.serveSession,
|
serveSession = self.props.serveSession,
|
||||||
showStringDiff = function(oldString: string, newString: string)
|
showStringDiff = function(currentString: string, incomingString: string)
|
||||||
self:setState({
|
self:setState({
|
||||||
showingStringDiff = true,
|
showingStringDiff = true,
|
||||||
oldString = oldString,
|
currentString = currentString,
|
||||||
newString = newString,
|
incomingString = incomingString,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
||||||
@@ -566,8 +566,8 @@ function ConnectedPage:render()
|
|||||||
anchorPoint = Vector2.new(0, 0),
|
anchorPoint = Vector2.new(0, 0),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
oldString = self.state.oldString,
|
currentString = self.state.currentString,
|
||||||
newString = self.state.newString,
|
incomingString = self.state.incomingString,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
local Spinner = require(Plugin.App.Components.Spinner)
|
local Spinner = require(Plugin.App.Components.Spinner)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
@@ -11,11 +13,35 @@ local e = Roact.createElement
|
|||||||
local ConnectingPage = Roact.Component:extend("ConnectingPage")
|
local ConnectingPage = Roact.Component:extend("ConnectingPage")
|
||||||
|
|
||||||
function ConnectingPage:render()
|
function ConnectingPage:render()
|
||||||
return e(Spinner, {
|
return Theme.with(function(theme)
|
||||||
position = UDim2.new(0.5, 0, 0.5, 0),
|
return e("Frame", {
|
||||||
anchorPoint = Vector2.new(0.5, 0.5),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
transparency = self.props.transparency,
|
BackgroundTransparency = 1,
|
||||||
})
|
}, {
|
||||||
|
Spinner = e(Spinner, {
|
||||||
|
position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
|
anchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}),
|
||||||
|
Text = if type(self.props.text) == "string" and #self.props.text > 0
|
||||||
|
then e("TextLabel", {
|
||||||
|
Text = self.props.text,
|
||||||
|
Position = UDim2.new(0.5, 0, 0.5, 30),
|
||||||
|
Size = UDim2.new(1, -40, 0.5, -40),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
|
RichText = true,
|
||||||
|
FontFace = theme.Font.Thin,
|
||||||
|
TextSize = theme.TextSize.Medium,
|
||||||
|
TextColor3 = theme.SubTextColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return ConnectingPage
|
return ConnectingPage
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -7,8 +5,10 @@ local Packages = Rojo.Packages
|
|||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
local Header = require(Plugin.App.Components.Header)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
@@ -24,43 +24,44 @@ function Error:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Error:render()
|
function Error:render()
|
||||||
return e(BorderedContainer, {
|
return Theme.with(function(theme)
|
||||||
size = Roact.joinBindings({
|
return e(BorderedContainer, {
|
||||||
containerSize = self.props.containerSize,
|
size = Roact.joinBindings({
|
||||||
contentSize = self.contentSize,
|
containerSize = self.props.containerSize,
|
||||||
}):map(function(values)
|
contentSize = self.contentSize,
|
||||||
local maximumSize = values.containerSize
|
}):map(function(values)
|
||||||
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
|
local maximumSize = values.containerSize
|
||||||
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
|
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
|
||||||
|
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
|
||||||
|
|
||||||
local outerSize = values.contentSize + ERROR_PADDING * 2
|
local outerSize = values.contentSize + ERROR_PADDING * 2
|
||||||
|
|
||||||
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
||||||
end),
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
}, {
|
|
||||||
ScrollingFrame = e(ScrollingFrame, {
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
|
||||||
contentSize = self.contentSize:map(function(value)
|
|
||||||
return value + ERROR_PADDING * 2
|
|
||||||
end),
|
end),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = self.props.layoutOrder,
|
||||||
[Roact.Change.AbsoluteSize] = function(object)
|
|
||||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(
|
|
||||||
self.props.errorMessage,
|
|
||||||
16,
|
|
||||||
Enum.Font.Code,
|
|
||||||
Vector2.new(containerSize.X, math.huge)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
|
||||||
end,
|
|
||||||
}, {
|
}, {
|
||||||
ErrorMessage = Theme.with(function(theme)
|
ScrollingFrame = e(ScrollingFrame, {
|
||||||
return e("TextBox", {
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
contentSize = self.contentSize:map(function(value)
|
||||||
|
return value + ERROR_PADDING * 2
|
||||||
|
end),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
[Roact.Change.AbsoluteSize] = function(object)
|
||||||
|
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||||
|
|
||||||
|
local textBounds = getTextBoundsAsync(
|
||||||
|
self.props.errorMessage,
|
||||||
|
theme.Font.Code,
|
||||||
|
theme.TextSize.Code,
|
||||||
|
containerSize.X
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
ErrorMessage = e("TextBox", {
|
||||||
[Roact.Event.InputBegan] = function(rbx, input)
|
[Roact.Event.InputBegan] = function(rbx, input)
|
||||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
||||||
return
|
return
|
||||||
@@ -71,8 +72,8 @@ function Error:render()
|
|||||||
|
|
||||||
Text = self.props.errorMessage,
|
Text = self.props.errorMessage,
|
||||||
TextEditable = false,
|
TextEditable = false,
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Code,
|
||||||
TextColor3 = theme.ErrorColor,
|
TextColor3 = theme.ErrorColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextYAlignment = Enum.TextYAlignment.Top,
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
@@ -81,17 +82,17 @@ function Error:render()
|
|||||||
ClearTextOnFocus = false,
|
ClearTextOnFocus = false,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
})
|
}),
|
||||||
end),
|
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
||||||
PaddingRight = UDim.new(0, ERROR_PADDING.X),
|
PaddingRight = UDim.new(0, ERROR_PADDING.X),
|
||||||
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
|
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
|
||||||
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
|
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
})
|
||||||
})
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local ErrorPage = Roact.Component:extend("ErrorPage")
|
local ErrorPage = Roact.Component:extend("ErrorPage")
|
||||||
@@ -109,16 +110,21 @@ function ErrorPage:render()
|
|||||||
self.setContainerSize(object.AbsoluteSize)
|
self.setContainerSize(object.AbsoluteSize)
|
||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
Error = e(Error, {
|
Header = e(Header, {
|
||||||
errorMessage = self.state.errorMessage,
|
|
||||||
containerSize = self.containerSize,
|
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 1,
|
layoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Error = e(Error, {
|
||||||
|
errorMessage = self.state.errorMessage,
|
||||||
|
containerSize = self.containerSize,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = 2,
|
||||||
|
}),
|
||||||
|
|
||||||
Buttons = e("Frame", {
|
Buttons = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 35),
|
Size = UDim2.new(1, 0, 0, 35),
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 3,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Close = e(TextButton, {
|
Close = e(TextButton, {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ local function AddressEntry(props)
|
|||||||
}, {
|
}, {
|
||||||
Host = e("TextBox", {
|
Host = e("TextBox", {
|
||||||
Text = props.host or "",
|
Text = props.host or "",
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.AddressEntry.TextColor,
|
TextColor3 = theme.AddressEntry.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
@@ -51,8 +51,8 @@ local function AddressEntry(props)
|
|||||||
|
|
||||||
Port = e("TextBox", {
|
Port = e("TextBox", {
|
||||||
Text = props.port or "",
|
Text = props.port or "",
|
||||||
Font = Enum.Font.Code,
|
FontFace = theme.Font.Code,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.AddressEntry.TextColor,
|
TextColor3 = theme.AddressEntry.TextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
PlaceholderText = Config.defaultPort,
|
PlaceholderText = Config.defaultPort,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local TextService = game:GetService("TextService")
|
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
@@ -9,6 +7,7 @@ local Roact = require(Packages.Roact)
|
|||||||
local Settings = require(Plugin.Settings)
|
local Settings = require(Plugin.Settings)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||||
|
|
||||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
local Checkbox = require(Plugin.App.Components.Checkbox)
|
||||||
local Dropdown = require(Plugin.App.Components.Dropdown)
|
local Dropdown = require(Plugin.App.Components.Dropdown)
|
||||||
@@ -31,10 +30,16 @@ local TAG_TYPES = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
local function getTextBoundsWithLineHeight(
|
||||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
text: string,
|
||||||
|
font: Font,
|
||||||
|
textSize: number,
|
||||||
|
width: number,
|
||||||
|
lineHeight: number
|
||||||
|
)
|
||||||
|
local textBounds = getTextBoundsAsync(text, font, textSize, width)
|
||||||
|
|
||||||
local lineCount = textBounds.Y / textSize
|
local lineCount = math.ceil(textBounds.Y / textSize)
|
||||||
local lineHeightAbsolute = textSize * lineHeight
|
local lineHeightAbsolute = textSize * lineHeight
|
||||||
|
|
||||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||||
@@ -109,6 +114,7 @@ function Setting:render()
|
|||||||
then self.props.input
|
then self.props.input
|
||||||
elseif self.props.options ~= nil then e(Dropdown, {
|
elseif self.props.options ~= nil then e(Dropdown, {
|
||||||
locked = self.props.locked,
|
locked = self.props.locked,
|
||||||
|
lockedTooltip = self.props.lockedTooltip,
|
||||||
options = self.props.options,
|
options = self.props.options,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
@@ -118,6 +124,7 @@ function Setting:render()
|
|||||||
})
|
})
|
||||||
else e(Checkbox, {
|
else e(Checkbox, {
|
||||||
locked = self.props.locked,
|
locked = self.props.locked,
|
||||||
|
lockedTooltip = self.props.lockedTooltip,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
onClick = function()
|
onClick = function()
|
||||||
@@ -145,7 +152,7 @@ function Setting:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Heading = e("Frame", {
|
Heading = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 16),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
@@ -165,8 +172,8 @@ function Setting:render()
|
|||||||
else nil,
|
else nil,
|
||||||
Name = e("TextLabel", {
|
Name = e("TextLabel", {
|
||||||
Text = self.props.name,
|
Text = self.props.name,
|
||||||
Font = Enum.Font.GothamBold,
|
FontFace = theme.Font.Bold,
|
||||||
TextSize = 16,
|
TextSize = theme.TextSize.Medium,
|
||||||
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
|
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
|
||||||
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
|
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
|
||||||
else settingsTheme.Setting.NameColor,
|
else settingsTheme.Setting.NameColor,
|
||||||
@@ -174,7 +181,7 @@ function Setting:render()
|
|||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
RichText = true,
|
RichText = true,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 16),
|
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||||
|
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -183,9 +190,9 @@ function Setting:render()
|
|||||||
|
|
||||||
Description = e("TextLabel", {
|
Description = e("TextLabel", {
|
||||||
Text = self.props.description,
|
Text = self.props.description,
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Main,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = theme.TextSize.Body,
|
||||||
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
@@ -197,12 +204,12 @@ function Setting:render()
|
|||||||
inputSize = self.inputSize,
|
inputSize = self.inputSize,
|
||||||
}):map(function(values)
|
}):map(function(values)
|
||||||
local offset = values.inputSize.X + 5
|
local offset = values.inputSize.X + 5
|
||||||
local textBounds = getTextBounds(
|
local textBounds = getTextBoundsWithLineHeight(
|
||||||
self.props.description,
|
self.props.description,
|
||||||
14,
|
theme.Font.Main,
|
||||||
Enum.Font.Gotham,
|
theme.TextSize.Body,
|
||||||
1.2,
|
values.containerSize.X - offset,
|
||||||
Vector2.new(values.containerSize.X - offset, math.huge)
|
1.2
|
||||||
)
|
)
|
||||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||||
end),
|
end),
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ end
|
|||||||
|
|
||||||
local invertedLevels = invertTbl(Log.Level)
|
local invertedLevels = invertTbl(Log.Level)
|
||||||
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
||||||
|
local syncReminderModes = { "None", "Notify", "Fullscreen" }
|
||||||
|
|
||||||
local function Navbar(props)
|
local function Navbar(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Settings.Navbar
|
local navbarTheme = theme.Settings.Navbar
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 46),
|
Size = UDim2.new(1, 0, 0, 46),
|
||||||
@@ -40,7 +41,7 @@ local function Navbar(props)
|
|||||||
Back = e(IconButton, {
|
Back = e(IconButton, {
|
||||||
icon = Assets.Images.Icons.Back,
|
icon = Assets.Images.Icons.Back,
|
||||||
iconSize = 24,
|
iconSize = 24,
|
||||||
color = theme.BackButtonColor,
|
color = navbarTheme.BackButtonColor,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
|
|
||||||
position = UDim2.new(0, 0, 0.5, 0),
|
position = UDim2.new(0, 0, 0.5, 0),
|
||||||
@@ -55,9 +56,9 @@ local function Navbar(props)
|
|||||||
|
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = "Settings",
|
Text = "Settings",
|
||||||
Font = Enum.Font.Gotham,
|
FontFace = theme.Font.Thin,
|
||||||
TextSize = 18,
|
TextSize = theme.TextSize.Large,
|
||||||
TextColor3 = theme.TextColor,
|
TextColor3 = navbarTheme.TextColor,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
@@ -81,185 +82,211 @@ function SettingsPage:render()
|
|||||||
return layoutOrder
|
return layoutOrder
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Roact.createFragment({
|
||||||
theme = theme.Settings
|
Navbar = e(Navbar, {
|
||||||
|
onBack = self.props.onBack,
|
||||||
return Roact.createFragment({
|
transparency = self.props.transparency,
|
||||||
Navbar = e(Navbar, {
|
layoutOrder = layoutIncrement(),
|
||||||
onBack = self.props.onBack,
|
}),
|
||||||
|
Content = e(ScrollingFrame, {
|
||||||
|
size = UDim2.new(1, 0, 1, -47),
|
||||||
|
position = UDim2.new(0, 0, 0, 47),
|
||||||
|
contentSize = self.contentSize,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
AutoReconnect = e(Setting, {
|
||||||
|
id = "autoReconnect",
|
||||||
|
name = "Auto Reconnect",
|
||||||
|
description = "Reconnect to server on place open if the served project matches the last sync to the place",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = layoutIncrement(),
|
layoutOrder = layoutIncrement(),
|
||||||
}),
|
}),
|
||||||
Content = e(ScrollingFrame, {
|
|
||||||
size = UDim2.new(1, 0, 1, -47),
|
ShowNotifications = e(Setting, {
|
||||||
position = UDim2.new(0, 0, 0, 47),
|
id = "showNotifications",
|
||||||
contentSize = self.contentSize,
|
name = "Show Notifications",
|
||||||
|
description = "Popup notifications in viewport",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
}, {
|
layoutOrder = layoutIncrement(),
|
||||||
ShowNotifications = e(Setting, {
|
}),
|
||||||
id = "showNotifications",
|
|
||||||
name = "Show Notifications",
|
|
||||||
description = "Popup notifications in viewport",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
SyncReminder = e(Setting, {
|
SyncReminderMode = e(Setting, {
|
||||||
id = "syncReminder",
|
id = "syncReminderMode",
|
||||||
name = "Sync Reminder",
|
name = "Sync Reminder",
|
||||||
description = "Notify to sync when opening a place that has previously been synced",
|
description = "What type of reminders you receive for syncing your project",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
visible = Settings:getBinding("showNotifications"),
|
layoutOrder = layoutIncrement(),
|
||||||
layoutOrder = layoutIncrement(),
|
visible = Settings:getBinding("showNotifications"),
|
||||||
}),
|
|
||||||
|
|
||||||
ConfirmationBehavior = e(Setting, {
|
options = syncReminderModes,
|
||||||
id = "confirmationBehavior",
|
}),
|
||||||
name = "Confirmation Behavior",
|
|
||||||
description = "When to prompt for confirmation before syncing",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
|
|
||||||
options = confirmationBehaviors,
|
SyncReminderPolling = e(Setting, {
|
||||||
}),
|
id = "syncReminderPolling",
|
||||||
|
name = "Sync Reminder Polling",
|
||||||
|
description = "Look for available sync servers periodically",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
visible = Settings:getBindings("syncReminderMode", "showNotifications"):map(function(values)
|
||||||
|
return values.syncReminderMode ~= "None" and values.showNotifications
|
||||||
|
end),
|
||||||
|
}),
|
||||||
|
|
||||||
LargeChangesConfirmationThreshold = e(Setting, {
|
ConfirmationBehavior = e(Setting, {
|
||||||
id = "largeChangesConfirmationThreshold",
|
id = "confirmationBehavior",
|
||||||
name = "Confirmation Threshold",
|
name = "Confirmation Behavior",
|
||||||
description = "How many modified instances to be considered a large change",
|
description = "When to prompt for confirmation before syncing",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = layoutIncrement(),
|
layoutOrder = layoutIncrement(),
|
||||||
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
|
|
||||||
return value == "Large Changes"
|
options = confirmationBehaviors,
|
||||||
|
}),
|
||||||
|
|
||||||
|
LargeChangesConfirmationThreshold = e(Setting, {
|
||||||
|
id = "largeChangesConfirmationThreshold",
|
||||||
|
name = "Confirmation Threshold",
|
||||||
|
description = "How many modified instances to be considered a large change",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
|
||||||
|
return value == "Large Changes"
|
||||||
|
end),
|
||||||
|
input = e(TextInput, {
|
||||||
|
size = UDim2.new(0, 40, 0, 28),
|
||||||
|
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
|
||||||
|
return tostring(value)
|
||||||
end),
|
end),
|
||||||
input = e(TextInput, {
|
|
||||||
size = UDim2.new(0, 40, 0, 28),
|
|
||||||
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
|
|
||||||
return tostring(value)
|
|
||||||
end),
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
enabled = true,
|
|
||||||
onEntered = function(text)
|
|
||||||
local number = tonumber(string.match(text, "%d+"))
|
|
||||||
if number then
|
|
||||||
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
|
|
||||||
else
|
|
||||||
-- Force text back to last valid value
|
|
||||||
Settings:set(
|
|
||||||
"largeChangesConfirmationThreshold",
|
|
||||||
Settings:get("largeChangesConfirmationThreshold")
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
PlaySounds = e(Setting, {
|
|
||||||
id = "playSounds",
|
|
||||||
name = "Play Sounds",
|
|
||||||
description = "Toggle sound effects",
|
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = layoutIncrement(),
|
enabled = true,
|
||||||
}),
|
onEntered = function(text)
|
||||||
|
local number = tonumber(string.match(text, "%d+"))
|
||||||
CheckForUpdates = e(Setting, {
|
if number then
|
||||||
id = "checkForUpdates",
|
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
|
||||||
name = "Check For Updates",
|
else
|
||||||
description = "Notify about newer compatible Rojo releases",
|
-- Force text back to last valid value
|
||||||
transparency = self.props.transparency,
|
Settings:set(
|
||||||
layoutOrder = layoutIncrement(),
|
"largeChangesConfirmationThreshold",
|
||||||
}),
|
Settings:get("largeChangesConfirmationThreshold")
|
||||||
|
)
|
||||||
CheckForPreleases = e(Setting, {
|
end
|
||||||
id = "checkForPrereleases",
|
|
||||||
name = "Include Prerelease Updates",
|
|
||||||
description = "Include prereleases when checking for updates",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
|
|
||||||
then false -- Must be a local install to allow prerelease checks
|
|
||||||
else Settings:getBinding("checkForUpdates"),
|
|
||||||
}),
|
|
||||||
|
|
||||||
AutoConnectPlaytestServer = e(Setting, {
|
|
||||||
id = "autoConnectPlaytestServer",
|
|
||||||
name = "Auto Connect Playtest Server",
|
|
||||||
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
|
|
||||||
tag = "unstable",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
OpenScriptsExternally = e(Setting, {
|
|
||||||
id = "openScriptsExternally",
|
|
||||||
name = "Open Scripts Externally",
|
|
||||||
description = "Attempt to open scripts in an external editor",
|
|
||||||
tag = "unstable",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
TwoWaySync = e(Setting, {
|
|
||||||
id = "twoWaySync",
|
|
||||||
name = "Two-Way Sync",
|
|
||||||
description = "Editing files in Studio will sync them into the filesystem",
|
|
||||||
locked = self.props.syncActive,
|
|
||||||
tag = "unstable",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
LogLevel = e(Setting, {
|
|
||||||
id = "logLevel",
|
|
||||||
name = "Log Level",
|
|
||||||
description = "Plugin output verbosity level",
|
|
||||||
tag = "debug",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
|
|
||||||
options = invertedLevels,
|
|
||||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
|
||||||
return value ~= "Info"
|
|
||||||
end),
|
|
||||||
onReset = function()
|
|
||||||
Settings:set("logLevel", "Info")
|
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
TypecheckingEnabled = e(Setting, {
|
|
||||||
id = "typecheckingEnabled",
|
|
||||||
name = "Typechecking",
|
|
||||||
description = "Toggle typechecking on the API surface",
|
|
||||||
tag = "debug",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
TimingLogsEnabled = e(Setting, {
|
|
||||||
id = "timingLogsEnabled",
|
|
||||||
name = "Timing Logs",
|
|
||||||
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
|
|
||||||
tag = "debug",
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = layoutIncrement(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
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)
|
PlaySounds = e(Setting, {
|
||||||
|
id = "playSounds",
|
||||||
|
name = "Play Sounds",
|
||||||
|
description = "Toggle sound effects",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
EnableSyncFallback = e(Setting, {
|
||||||
|
id = "enableSyncFallback",
|
||||||
|
name = "Enable Sync Fallback",
|
||||||
|
description = "Whether Instances that fail to sync are remade as a fallback. If this is enabled, Instances may be destroyed and remade when syncing.",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
CheckForUpdates = e(Setting, {
|
||||||
|
id = "checkForUpdates",
|
||||||
|
name = "Check For Updates",
|
||||||
|
description = "Notify about newer compatible Rojo releases",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
CheckForPreleases = e(Setting, {
|
||||||
|
id = "checkForPrereleases",
|
||||||
|
name = "Include Prerelease Updates",
|
||||||
|
description = "Include prereleases when checking for updates",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
|
||||||
|
then false -- Must be a local install to allow prerelease checks
|
||||||
|
else Settings:getBinding("checkForUpdates"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
AutoConnectPlaytestServer = e(Setting, {
|
||||||
|
id = "autoConnectPlaytestServer",
|
||||||
|
name = "Auto Connect Playtest Server",
|
||||||
|
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
|
||||||
|
tag = "unstable",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
OpenScriptsExternally = e(Setting, {
|
||||||
|
id = "openScriptsExternally",
|
||||||
|
name = "Open Scripts Externally",
|
||||||
|
description = "Attempt to open scripts in an external editor",
|
||||||
|
tag = "unstable",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
TwoWaySync = e(Setting, {
|
||||||
|
id = "twoWaySync",
|
||||||
|
name = "Two-Way Sync",
|
||||||
|
description = "Editing files in Studio will sync them into the filesystem",
|
||||||
|
locked = self.props.syncActive,
|
||||||
|
lockedTooltip = "(Cannot change while currently syncing. Disconnect first.)",
|
||||||
|
tag = "unstable",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
LogLevel = e(Setting, {
|
||||||
|
id = "logLevel",
|
||||||
|
name = "Log Level",
|
||||||
|
description = "Plugin output verbosity level",
|
||||||
|
tag = "debug",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
|
||||||
|
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",
|
||||||
|
tag = "debug",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
TimingLogsEnabled = e(Setting, {
|
||||||
|
id = "timingLogsEnabled",
|
||||||
|
name = "Timing Logs",
|
||||||
|
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
|
||||||
|
tag = "debug",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
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
|
return SettingsPage
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
--[[
|
--[[
|
||||||
Theming system taking advantage of Roact's new context API.
|
Theming system provided through Roact's context.
|
||||||
Doesn't use colors provided by Studio and instead just branches on theme
|
Uses Studio colors when possible.
|
||||||
name. This isn't exactly best practice.
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
|
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
|
||||||
@@ -15,6 +14,8 @@ local function getStudio()
|
|||||||
return _Studio
|
return _Studio
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local ContentProvider = game:GetService("ContentProvider")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
@@ -35,6 +36,27 @@ function StudioProvider:updateTheme()
|
|||||||
local isDark = studioTheme.Name == "Dark"
|
local isDark = studioTheme.Name == "Dark"
|
||||||
|
|
||||||
local theme = strict(studioTheme.Name .. "Theme", {
|
local theme = strict(studioTheme.Name .. "Theme", {
|
||||||
|
Font = {
|
||||||
|
Main = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Medium, Enum.FontStyle.Normal),
|
||||||
|
Bold = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
|
||||||
|
Thin = Font.new(
|
||||||
|
"rbxasset://fonts/families/Montserrat.json",
|
||||||
|
Enum.FontWeight.Regular,
|
||||||
|
Enum.FontStyle.Normal
|
||||||
|
),
|
||||||
|
Code = Font.new(
|
||||||
|
"rbxasset://fonts/families/Inconsolata.json",
|
||||||
|
Enum.FontWeight.Regular,
|
||||||
|
Enum.FontStyle.Normal
|
||||||
|
),
|
||||||
|
},
|
||||||
|
TextSize = {
|
||||||
|
Body = 15,
|
||||||
|
Small = 13,
|
||||||
|
Medium = 16,
|
||||||
|
Large = 18,
|
||||||
|
Code = 16,
|
||||||
|
},
|
||||||
BrandColor = BRAND_COLOR,
|
BrandColor = BRAND_COLOR,
|
||||||
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
||||||
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
@@ -143,12 +165,29 @@ function StudioProvider:updateTheme()
|
|||||||
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
-- Studio doesn't have good colors since their diffs use backgrounds, not text
|
-- Very bright different colors in case some places were not updated to use
|
||||||
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
|
-- the new background diff colors.
|
||||||
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
|
Add = Color3.fromRGB(255, 0, 255),
|
||||||
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
|
Remove = Color3.fromRGB(255, 0, 255),
|
||||||
|
Edit = Color3.fromRGB(255, 0, 255),
|
||||||
|
|
||||||
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
||||||
|
|
||||||
|
Background = {
|
||||||
|
-- Studio doesn't have good colors since their diffs use backgrounds, not text
|
||||||
|
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
|
||||||
|
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
|
||||||
|
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
|
||||||
|
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
},
|
||||||
|
|
||||||
|
Text = {
|
||||||
|
Add = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||||
|
Remove = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||||
|
Edit = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||||
|
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ConnectionDetails = {
|
ConnectionDetails = {
|
||||||
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
@@ -190,6 +229,13 @@ end
|
|||||||
|
|
||||||
function StudioProvider:init()
|
function StudioProvider:init()
|
||||||
self:updateTheme()
|
self:updateTheme()
|
||||||
|
|
||||||
|
-- Preload the Fonts so that getTextBoundsAsync won't yield
|
||||||
|
local fontAssetIds = {}
|
||||||
|
for _, font in self.state.theme.Font do
|
||||||
|
table.insert(fontAssetIds, font.Family)
|
||||||
|
end
|
||||||
|
pcall(ContentProvider.PreloadAsync, ContentProvider, fontAssetIds)
|
||||||
end
|
end
|
||||||
|
|
||||||
function StudioProvider:render()
|
function StudioProvider:render()
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ end
|
|||||||
local function blendAlpha(alphaValues)
|
local function blendAlpha(alphaValues)
|
||||||
local alpha = 0
|
local alpha = 0
|
||||||
|
|
||||||
for _, value in pairs(alphaValues) do
|
for _, value in alphaValues do
|
||||||
alpha = alpha + (1 - alpha) * value
|
alpha = alpha + (1 - alpha) * value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
41
plugin/src/App/getTextBoundsAsync.lua
Normal file
41
plugin/src/App/getTextBoundsAsync.lua
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
|
local params = Instance.new("GetTextBoundsParams")
|
||||||
|
|
||||||
|
local function getTextBoundsAsync(
|
||||||
|
text: string,
|
||||||
|
font: Font,
|
||||||
|
textSize: number,
|
||||||
|
width: number,
|
||||||
|
richText: boolean?
|
||||||
|
): Vector2
|
||||||
|
if type(text) ~= "string" then
|
||||||
|
Log.warn(`Invalid text. Expected string, received {type(text)} instead`)
|
||||||
|
return Vector2.zero
|
||||||
|
end
|
||||||
|
if #text >= 200_000 then
|
||||||
|
Log.warn(`Invalid text. Exceeds the 199,999 character limit`)
|
||||||
|
return Vector2.zero
|
||||||
|
end
|
||||||
|
|
||||||
|
params.Text = text
|
||||||
|
params.Font = font
|
||||||
|
params.Size = textSize
|
||||||
|
params.Width = width
|
||||||
|
params.RichText = not not richText
|
||||||
|
|
||||||
|
local success, bounds = pcall(TextService.GetTextBoundsAsync, TextService, params)
|
||||||
|
if not success then
|
||||||
|
Log.warn(`Failed to get text bounds: {bounds}`)
|
||||||
|
return Vector2.zero
|
||||||
|
end
|
||||||
|
|
||||||
|
return bounds
|
||||||
|
end
|
||||||
|
|
||||||
|
return getTextBoundsAsync
|
||||||
@@ -9,6 +9,7 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
local Promise = require(Packages.Promise)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Version = require(Plugin.Version)
|
local Version = require(Plugin.Version)
|
||||||
@@ -27,7 +28,7 @@ local timeUtil = require(Plugin.timeUtil)
|
|||||||
local Theme = require(script.Theme)
|
local Theme = require(script.Theme)
|
||||||
|
|
||||||
local Page = require(script.Page)
|
local Page = require(script.Page)
|
||||||
local Notifications = require(script.Notifications)
|
local Notifications = require(script.Components.Notifications)
|
||||||
local Tooltip = require(script.Components.Tooltip)
|
local Tooltip = require(script.Components.Tooltip)
|
||||||
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
||||||
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
||||||
@@ -52,9 +53,9 @@ local App = Roact.Component:extend("App")
|
|||||||
function App:init()
|
function App:init()
|
||||||
preloadAssets()
|
preloadAssets()
|
||||||
|
|
||||||
local priorHost, priorPort = self:getPriorEndpoint()
|
local priorSyncInfo = self:getPriorSyncInfo()
|
||||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
self.host, self.setHost = Roact.createBinding(priorSyncInfo.host or "")
|
||||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
self.port, self.setPort = Roact.createBinding(priorSyncInfo.port or "")
|
||||||
|
|
||||||
self.confirmationBindable = Instance.new("BindableEvent")
|
self.confirmationBindable = Instance.new("BindableEvent")
|
||||||
self.confirmationEvent = self.confirmationBindable.Event
|
self.confirmationEvent = self.confirmationBindable.Event
|
||||||
@@ -78,17 +79,18 @@ function App:init()
|
|||||||
action
|
action
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
local dismissNotif = self:addNotification(
|
local dismissNotif = self:addNotification({
|
||||||
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
text = string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||||
10,
|
timeout = 10,
|
||||||
{
|
onClose = function()
|
||||||
|
cleanup()
|
||||||
|
end,
|
||||||
|
actions = {
|
||||||
Restore = {
|
Restore = {
|
||||||
text = "Restore",
|
text = "Restore",
|
||||||
style = "Solid",
|
style = "Solid",
|
||||||
layoutOrder = 1,
|
layoutOrder = 1,
|
||||||
onClick = function(notification)
|
onClick = function()
|
||||||
cleanup()
|
|
||||||
notification:dismiss()
|
|
||||||
ChangeHistoryService:Redo()
|
ChangeHistoryService:Redo()
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -96,13 +98,9 @@ function App:init()
|
|||||||
text = "Dismiss",
|
text = "Dismiss",
|
||||||
style = "Bordered",
|
style = "Bordered",
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
onClick = function(notification)
|
|
||||||
cleanup()
|
|
||||||
notification:dismiss()
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
|
|
||||||
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
||||||
-- Our notif is now out of date- redoing will not restore the patch
|
-- Our notif is now out of date- redoing will not restore the patch
|
||||||
@@ -142,32 +140,20 @@ function App:init()
|
|||||||
if RunService:IsEdit() then
|
if RunService:IsEdit() then
|
||||||
self:checkForUpdates()
|
self:checkForUpdates()
|
||||||
|
|
||||||
if
|
self:startSyncReminderPolling()
|
||||||
Settings:get("syncReminder")
|
self.disconnectSyncReminderPollingChanged = Settings:onChanged("syncReminderPolling", function(enabled)
|
||||||
and self.serveSession == nil
|
if enabled then
|
||||||
and self:getLastSyncTimestamp()
|
self:startSyncReminderPolling()
|
||||||
and (self:isSyncLockAvailable())
|
else
|
||||||
then
|
self:stopSyncReminderPolling()
|
||||||
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
|
end
|
||||||
Connect = {
|
end)
|
||||||
text = "Connect",
|
|
||||||
style = "Solid",
|
self:tryAutoReconnect():andThen(function(didReconnect)
|
||||||
layoutOrder = 1,
|
if not didReconnect then
|
||||||
onClick = function(notification)
|
self:checkSyncReminder()
|
||||||
notification:dismiss()
|
end
|
||||||
self:startSession()
|
end)
|
||||||
end,
|
|
||||||
},
|
|
||||||
Dismiss = {
|
|
||||||
text = "Dismiss",
|
|
||||||
style = "Bordered",
|
|
||||||
layoutOrder = 2,
|
|
||||||
onClick = function(notification)
|
|
||||||
notification:dismiss()
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if self:isAutoConnectPlaytestServerAvailable() then
|
if self:isAutoConnectPlaytestServerAvailable() then
|
||||||
@@ -188,21 +174,30 @@ function App:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function App:willUnmount()
|
function App:willUnmount()
|
||||||
|
self:endSession()
|
||||||
|
|
||||||
self.waypointConnection:Disconnect()
|
self.waypointConnection:Disconnect()
|
||||||
self.confirmationBindable:Destroy()
|
self.confirmationBindable:Destroy()
|
||||||
|
|
||||||
self.disconnectUpdatesCheckChanged()
|
self.disconnectUpdatesCheckChanged()
|
||||||
self.disconnectPrereleasesCheckChanged()
|
self.disconnectPrereleasesCheckChanged()
|
||||||
|
if self.disconnectSyncReminderPollingChanged then
|
||||||
|
self.disconnectSyncReminderPollingChanged()
|
||||||
|
end
|
||||||
|
|
||||||
|
self:stopSyncReminderPolling()
|
||||||
|
|
||||||
self.autoConnectPlaytestServerListener()
|
self.autoConnectPlaytestServerListener()
|
||||||
self:clearRunningConnectionInfo()
|
self:clearRunningConnectionInfo()
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:addNotification(
|
function App:addNotification(notif: {
|
||||||
text: string,
|
text: string,
|
||||||
|
isFullscreen: boolean?,
|
||||||
timeout: number?,
|
timeout: number?,
|
||||||
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
|
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> ()? } }?,
|
||||||
)
|
onClose: (any) -> ()?,
|
||||||
|
})
|
||||||
if not Settings:get("showNotifications") then
|
if not Settings:get("showNotifications") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -210,17 +205,17 @@ function App:addNotification(
|
|||||||
self.notifId += 1
|
self.notifId += 1
|
||||||
local id = self.notifId
|
local id = self.notifId
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
self:setState(function(prevState)
|
||||||
notifications[id] = {
|
local notifications = table.clone(prevState.notifications)
|
||||||
text = text,
|
notifications[id] = Dictionary.merge({
|
||||||
timestamp = DateTime.now().UnixTimestampMillis,
|
timeout = notif.timeout or 5,
|
||||||
timeout = timeout or 3,
|
isFullscreen = notif.isFullscreen or false,
|
||||||
actions = actions,
|
}, notif)
|
||||||
}
|
|
||||||
|
|
||||||
self:setState({
|
return {
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
self:closeNotification(id)
|
self:closeNotification(id)
|
||||||
@@ -232,96 +227,60 @@ function App:closeNotification(id: number)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
self:setState(function(prevState)
|
||||||
notifications[id] = nil
|
local notifications = table.clone(prevState.notifications)
|
||||||
|
notifications[id] = nil
|
||||||
|
|
||||||
self:setState({
|
return {
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
}
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:checkForUpdates()
|
function App:checkForUpdates()
|
||||||
if not Settings:get("checkForUpdates") then
|
local updateMessage = Version.getUpdateMessage()
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil
|
if updateMessage then
|
||||||
local latestCompatibleVersion = Version.retrieveLatestCompatible({
|
self:addNotification({
|
||||||
version = Config.version,
|
text = updateMessage,
|
||||||
includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"),
|
timeout = 500,
|
||||||
})
|
actions = {
|
||||||
if not latestCompatibleVersion then
|
Dismiss = {
|
||||||
return
|
text = "Dismiss",
|
||||||
end
|
style = "Bordered",
|
||||||
|
layoutOrder = 2,
|
||||||
self:addNotification(
|
},
|
||||||
string.format(
|
|
||||||
"A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.",
|
|
||||||
Version.display(latestCompatibleVersion.version),
|
|
||||||
timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp)
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
{
|
|
||||||
Dismiss = {
|
|
||||||
text = "Dismiss",
|
|
||||||
style = "Bordered",
|
|
||||||
layoutOrder = 2,
|
|
||||||
onClick = function(notification)
|
|
||||||
notification:dismiss()
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
)
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getPriorEndpoint()
|
function App:getPriorSyncInfo(): { host: string?, port: string?, projectName: string?, timestamp: number? }
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then
|
if not priorSyncInfos then
|
||||||
return
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local id = tostring(game.PlaceId)
|
||||||
if ignorePlaceIds[id] then
|
if ignorePlaceIds[id] then
|
||||||
return
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local place = priorEndpoints[id]
|
return priorSyncInfos[id] or {}
|
||||||
if not place then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return place.host, place.port
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getLastSyncTimestamp()
|
function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then
|
if not priorSyncInfos then
|
||||||
return
|
priorSyncInfos = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local id = tostring(game.PlaceId)
|
local now = os.time()
|
||||||
if ignorePlaceIds[id] then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local place = priorEndpoints[id]
|
|
||||||
if not place then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return place.timestamp
|
|
||||||
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
|
-- Clear any stale saves to avoid disc bloat
|
||||||
for placeId, endpoint in priorEndpoints do
|
for placeId, syncInfo in priorSyncInfos do
|
||||||
if os.time() - endpoint.timestamp > 12_960_000 then
|
if now - (syncInfo.timestamp or now) > 12_960_000 then
|
||||||
priorEndpoints[placeId] = nil
|
priorSyncInfos[placeId] = nil
|
||||||
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -331,14 +290,15 @@ function App:setPriorEndpoint(host: string, port: string)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
priorEndpoints[id] = {
|
priorSyncInfos[id] = {
|
||||||
host = if host ~= Config.defaultHost then host else nil,
|
host = if host ~= Config.defaultHost then host else nil,
|
||||||
port = if port ~= Config.defaultPort then port else nil,
|
port = if port ~= Config.defaultPort then port else nil,
|
||||||
timestamp = os.time(),
|
projectName = projectName,
|
||||||
|
timestamp = now,
|
||||||
}
|
}
|
||||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||||
|
|
||||||
Settings:set("priorEndpoints", priorEndpoints)
|
Settings:set("priorEndpoints", priorSyncInfos)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:getHostAndPort()
|
function App:getHostAndPort()
|
||||||
@@ -413,8 +373,158 @@ function App:releaseSyncLock()
|
|||||||
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function App:findActiveServer()
|
||||||
|
local host, port = self:getHostAndPort()
|
||||||
|
local baseUrl = if string.find(host, "^https?://")
|
||||||
|
then string.format("%s:%s", host, port)
|
||||||
|
else string.format("http://%s:%s", host, port)
|
||||||
|
|
||||||
|
Log.trace("Checking for active sync server at {}", baseUrl)
|
||||||
|
|
||||||
|
local apiContext = ApiContext.new(baseUrl)
|
||||||
|
return apiContext:connect():andThen(function(serverInfo)
|
||||||
|
apiContext:disconnect()
|
||||||
|
return serverInfo, host, port
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:tryAutoReconnect()
|
||||||
|
if not Settings:get("autoReconnect") then
|
||||||
|
return Promise.resolve(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
local priorSyncInfo = self:getPriorSyncInfo()
|
||||||
|
if not priorSyncInfo.projectName then
|
||||||
|
Log.trace("No prior sync info found, skipping auto-reconnect")
|
||||||
|
return Promise.resolve(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
return self:findActiveServer()
|
||||||
|
:andThen(function(serverInfo)
|
||||||
|
-- change
|
||||||
|
if serverInfo.projectName == priorSyncInfo.projectName then
|
||||||
|
Log.trace("Auto-reconnect found matching server, reconnecting...")
|
||||||
|
self:addNotification({
|
||||||
|
text = `Auto-reconnect discovered project '{serverInfo.projectName}'...`,
|
||||||
|
})
|
||||||
|
self:startSession()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
Log.trace("Auto-reconnect found different server, not reconnecting")
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
:catch(function()
|
||||||
|
Log.trace("Auto-reconnect did not find a server, not reconnecting")
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:checkSyncReminder()
|
||||||
|
local syncReminderMode = Settings:get("syncReminderMode")
|
||||||
|
if syncReminderMode == "None" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.serveSession ~= nil or not self:isSyncLockAvailable() then
|
||||||
|
-- Already syncing or cannot sync, no reason to remind
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local priorSyncInfo = self:getPriorSyncInfo()
|
||||||
|
|
||||||
|
self:findActiveServer()
|
||||||
|
:andThen(function(serverInfo, host, port)
|
||||||
|
self:sendSyncReminder(
|
||||||
|
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
:catch(function()
|
||||||
|
if priorSyncInfo.timestamp and priorSyncInfo.projectName then
|
||||||
|
-- We didn't find an active server,
|
||||||
|
-- but this place has a prior sync
|
||||||
|
-- so we should remind the user to serve
|
||||||
|
|
||||||
|
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
||||||
|
self:sendSyncReminder(
|
||||||
|
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:startSyncReminderPolling()
|
||||||
|
if
|
||||||
|
self.syncReminderPollingThread ~= nil
|
||||||
|
or Settings:get("syncReminderMode") == "None"
|
||||||
|
or not Settings:get("syncReminderPolling")
|
||||||
|
then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.trace("Starting sync reminder polling thread")
|
||||||
|
self.syncReminderPollingThread = task.spawn(function()
|
||||||
|
while task.wait(30) do
|
||||||
|
if self.syncReminderPollingThread == nil then
|
||||||
|
-- The polling thread was stopped, so exit
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self.dismissSyncReminder then
|
||||||
|
-- There is already a sync reminder being shown
|
||||||
|
task.wait(5)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
self:checkSyncReminder()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:stopSyncReminderPolling()
|
||||||
|
if self.syncReminderPollingThread then
|
||||||
|
Log.trace("Stopping sync reminder polling thread")
|
||||||
|
task.cancel(self.syncReminderPollingThread)
|
||||||
|
self.syncReminderPollingThread = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:sendSyncReminder(message: string)
|
||||||
|
local syncReminderMode = Settings:get("syncReminderMode")
|
||||||
|
if syncReminderMode == "None" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.dismissSyncReminder = self:addNotification({
|
||||||
|
text = message,
|
||||||
|
timeout = 120,
|
||||||
|
isFullscreen = Settings:get("syncReminderMode") == "Fullscreen",
|
||||||
|
onClose = function()
|
||||||
|
self.dismissSyncReminder = nil
|
||||||
|
end,
|
||||||
|
actions = {
|
||||||
|
Connect = {
|
||||||
|
text = "Connect",
|
||||||
|
style = "Solid",
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = function()
|
||||||
|
self:startSession()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
Dismiss = {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = function()
|
||||||
|
-- If the user dismisses the reminder,
|
||||||
|
-- then we don't need to remind them again
|
||||||
|
self:stopSyncReminderPolling()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
function App:isAutoConnectPlaytestServerAvailable()
|
function App:isAutoConnectPlaytestServerAvailable()
|
||||||
return RunService:IsRunMode()
|
return RunService:IsRunning()
|
||||||
|
and RunService:IsStudio()
|
||||||
and RunService:IsServer()
|
and RunService:IsServer()
|
||||||
and Settings:get("autoConnectPlaytestServer")
|
and Settings:get("autoConnectPlaytestServer")
|
||||||
and workspace:GetAttribute("__Rojo_ConnectionUrl")
|
and workspace:GetAttribute("__Rojo_ConnectionUrl")
|
||||||
@@ -462,7 +572,10 @@ function App:startSession()
|
|||||||
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
||||||
|
|
||||||
Log.warn(msg)
|
Log.warn(msg)
|
||||||
self:addNotification(msg, 10)
|
self:addNotification({
|
||||||
|
text = msg,
|
||||||
|
timeout = 10,
|
||||||
|
})
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Error,
|
appStatus = AppStatus.Error,
|
||||||
errorMessage = msg,
|
errorMessage = msg,
|
||||||
@@ -484,64 +597,62 @@ function App:startSession()
|
|||||||
twoWaySync = Settings:get("twoWaySync"),
|
twoWaySync = Settings:get("twoWaySync"),
|
||||||
})
|
})
|
||||||
|
|
||||||
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
|
serveSession:setUpdateLoadingTextCallback(function(text: string)
|
||||||
|
self:setState({
|
||||||
|
connectingText = text,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
|
||||||
-- Build new tree for patch
|
-- Build new tree for patch
|
||||||
self:setState({
|
self:setState({
|
||||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
self.cleanupPostcommit = serveSession:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||||
-- Update tree with unapplied metadata
|
local now = DateTime.now().UnixTimestamp
|
||||||
self:setState(function(prevState)
|
self:setState(function(prevState)
|
||||||
|
local oldPatchData = prevState.patchData
|
||||||
|
local newPatchData = {
|
||||||
|
patch = patch,
|
||||||
|
unapplied = unappliedPatch,
|
||||||
|
timestamp = now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if PatchSet.isEmpty(patch) then
|
||||||
|
-- Keep existing patch info, but use new timestamp
|
||||||
|
newPatchData.patch = oldPatchData.patch
|
||||||
|
newPatchData.unapplied = oldPatchData.unapplied
|
||||||
|
elseif now - oldPatchData.timestamp < 2 then
|
||||||
|
-- Patches that apply in the same second are combined for human clarity
|
||||||
|
newPatchData.patch = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.patch, patch)
|
||||||
|
newPatchData.unapplied = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.unapplied, unappliedPatch)
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
||||||
|
patchData = newPatchData,
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
|
|
||||||
local now = DateTime.now().UnixTimestamp
|
|
||||||
local old = self.state.patchData
|
|
||||||
|
|
||||||
if PatchSet.isEmpty(patch) then
|
|
||||||
-- Ignore empty patch, but update timestamp
|
|
||||||
self:setState({
|
|
||||||
patchData = {
|
|
||||||
patch = old.patch,
|
|
||||||
unapplied = old.unapplied,
|
|
||||||
timestamp = now,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
|
|
||||||
unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
|
|
||||||
end
|
|
||||||
|
|
||||||
self:setState({
|
|
||||||
patchData = {
|
|
||||||
patch = patch,
|
|
||||||
unapplied = unapplied,
|
|
||||||
timestamp = now,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
serveSession:onStatusChanged(function(status, details)
|
serveSession:onStatusChanged(function(status, details)
|
||||||
if status == ServeSession.Status.Connecting then
|
if status == ServeSession.Status.Connecting then
|
||||||
self:setPriorEndpoint(host, port)
|
if self.dismissSyncReminder then
|
||||||
|
self.dismissSyncReminder()
|
||||||
|
self.dismissSyncReminder = nil
|
||||||
|
end
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Connecting,
|
appStatus = AppStatus.Connecting,
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
self:addNotification("Connecting to session...")
|
self:addNotification({
|
||||||
|
text = "Connecting to session...",
|
||||||
|
})
|
||||||
elseif status == ServeSession.Status.Connected then
|
elseif status == ServeSession.Status.Connected then
|
||||||
self.knownProjects[details] = true
|
self.knownProjects[details] = true
|
||||||
|
self:setPriorSyncInfo(host, port, details)
|
||||||
self:setRunningConnectionInfo(baseUrl)
|
self:setRunningConnectionInfo(baseUrl)
|
||||||
|
|
||||||
local address = ("%s:%s"):format(host, port)
|
local address = ("%s:%s"):format(host, port)
|
||||||
@@ -551,7 +662,9 @@ function App:startSession()
|
|||||||
address = address,
|
address = address,
|
||||||
toolbarIcon = Assets.Images.PluginButtonConnected,
|
toolbarIcon = Assets.Images.PluginButtonConnected,
|
||||||
})
|
})
|
||||||
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
self:addNotification({
|
||||||
|
text = string.format("Connected to session '%s' at %s.", details, address),
|
||||||
|
})
|
||||||
elseif status == ServeSession.Status.Disconnected then
|
elseif status == ServeSession.Status.Disconnected then
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
self:releaseSyncLock()
|
self:releaseSyncLock()
|
||||||
@@ -574,13 +687,19 @@ function App:startSession()
|
|||||||
errorMessage = tostring(details),
|
errorMessage = tostring(details),
|
||||||
toolbarIcon = Assets.Images.PluginButtonWarning,
|
toolbarIcon = Assets.Images.PluginButtonWarning,
|
||||||
})
|
})
|
||||||
self:addNotification(tostring(details), 10)
|
self:addNotification({
|
||||||
|
text = tostring(details),
|
||||||
|
timeout = 10,
|
||||||
|
})
|
||||||
else
|
else
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
self:addNotification("Disconnected from session.")
|
self:addNotification({
|
||||||
|
text = "Disconnected from session.",
|
||||||
|
timeout = 10,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -648,23 +767,25 @@ function App:startSession()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
connectingText = "Computing diff view...",
|
||||||
|
})
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Confirming,
|
appStatus = AppStatus.Confirming,
|
||||||
|
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }),
|
||||||
confirmData = {
|
confirmData = {
|
||||||
instanceMap = instanceMap,
|
|
||||||
patch = patch,
|
|
||||||
serverInfo = serverInfo,
|
serverInfo = serverInfo,
|
||||||
},
|
},
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
|
|
||||||
self:addNotification(
|
self:addNotification({
|
||||||
string.format(
|
text = string.format(
|
||||||
"Please accept%sor abort the initializing sync session.",
|
"Please accept%sor abort the initializing sync session.",
|
||||||
Settings:get("twoWaySync") and ", reject, " or " "
|
Settings:get("twoWaySync") and ", reject, " or " "
|
||||||
),
|
),
|
||||||
7
|
timeout = 7,
|
||||||
)
|
})
|
||||||
|
|
||||||
return self.confirmationEvent:Wait()
|
return self.confirmationEvent:Wait()
|
||||||
end)
|
end)
|
||||||
@@ -763,6 +884,7 @@ function App:render()
|
|||||||
|
|
||||||
ConfirmingPage = createPageElement(AppStatus.Confirming, {
|
ConfirmingPage = createPageElement(AppStatus.Confirming, {
|
||||||
confirmData = self.state.confirmData,
|
confirmData = self.state.confirmData,
|
||||||
|
patchTree = self.state.patchTree,
|
||||||
createPopup = not self.state.guiEnabled,
|
createPopup = not self.state.guiEnabled,
|
||||||
|
|
||||||
onAbort = function()
|
onAbort = function()
|
||||||
@@ -776,7 +898,9 @@ function App:render()
|
|||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Connecting = createPageElement(AppStatus.Connecting),
|
Connecting = createPageElement(AppStatus.Connecting, {
|
||||||
|
text = self.state.connectingText,
|
||||||
|
}),
|
||||||
|
|
||||||
Connected = createPageElement(AppStatus.Connected, {
|
Connected = createPageElement(AppStatus.Connected, {
|
||||||
projectName = self.state.projectName,
|
projectName = self.state.projectName,
|
||||||
@@ -825,19 +949,7 @@ function App:render()
|
|||||||
ResetOnSpawn = false,
|
ResetOnSpawn = false,
|
||||||
DisplayOrder = 100,
|
DisplayOrder = 100,
|
||||||
}, {
|
}, {
|
||||||
layout = e("UIListLayout", {
|
Notifications = e(Notifications, {
|
||||||
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,
|
soundPlayer = self.props.soundPlayer,
|
||||||
notifications = self.state.notifications,
|
notifications = self.state.notifications,
|
||||||
onClose = function(id)
|
onClose = function(id)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ local Assets = {
|
|||||||
local function guardForTypos(name, map)
|
local function guardForTypos(name, map)
|
||||||
strict(name, map)
|
strict(name, map)
|
||||||
|
|
||||||
for key, child in pairs(map) do
|
for key, child in map do
|
||||||
if type(child) == "table" then
|
if type(child) == "table" then
|
||||||
guardForTypos(("%s.%s"):format(name, key), child)
|
guardForTypos(("%s.%s"):format(name, key), child)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ local encodePatchUpdate = require(script.Parent.encodePatchUpdate)
|
|||||||
return function(instanceMap, propertyChanges)
|
return function(instanceMap, propertyChanges)
|
||||||
local patch = PatchSet.newEmpty()
|
local patch = PatchSet.newEmpty()
|
||||||
|
|
||||||
for instance, properties in pairs(propertyChanges) do
|
for instance, properties in propertyChanges do
|
||||||
local instanceId = instanceMap.fromInstances[instance]
|
local instanceId = instanceMap.fromInstances[instance]
|
||||||
|
|
||||||
if instanceId == nil then
|
if instanceId == nil then
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ return function(instance, instanceId, properties)
|
|||||||
changedProperties = {},
|
changedProperties = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for propertyName in pairs(properties) do
|
for propertyName in properties do
|
||||||
if propertyName == "Name" then
|
if propertyName == "Name" then
|
||||||
update.changedName = instance.Name
|
update.changedName = instance.Name
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ return strict("Config", {
|
|||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = realVersion,
|
version = realVersion,
|
||||||
expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]),
|
expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]),
|
||||||
protocolVersion = 4,
|
protocolVersion = 5,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
defaultPort = "34872",
|
defaultPort = "34872",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ local function merge(...)
|
|||||||
local source = select(i, ...)
|
local source = select(i, ...)
|
||||||
|
|
||||||
if source ~= nil then
|
if source ~= nil then
|
||||||
for key, value in pairs(source) do
|
for key, value in source do
|
||||||
if value == None then
|
if value == None then
|
||||||
output[key] = nil
|
output[key] = nil
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function InstanceMap:__fmtDebug(output)
|
|||||||
-- Collect all of the entries in the InstanceMap and sort them by their
|
-- Collect all of the entries in the InstanceMap and sort them by their
|
||||||
-- label, which helps make our output deterministic.
|
-- label, which helps make our output deterministic.
|
||||||
local entries = {}
|
local entries = {}
|
||||||
for id, instance in pairs(self.fromIds) do
|
for id, instance in self.fromIds do
|
||||||
local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName)
|
local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName)
|
||||||
|
|
||||||
table.insert(entries, { id, label })
|
table.insert(entries, { id, label })
|
||||||
@@ -73,7 +73,7 @@ function InstanceMap:__fmtDebug(output)
|
|||||||
return a[2] < b[2]
|
return a[2] < b[2]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
for _, entry in ipairs(entries) do
|
for _, entry in entries do
|
||||||
output:writeLine("{}: {}", entry[1], entry[2])
|
output:writeLine("{}: {}", entry[1], entry[2])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ function InstanceMap:__disconnectSignals(instance)
|
|||||||
-- around the extra table. ValueBase objects force us to use multiple
|
-- around the extra table. ValueBase objects force us to use multiple
|
||||||
-- signals to emulate the Instance.Changed event, however.
|
-- signals to emulate the Instance.Changed event, however.
|
||||||
if typeof(signals) == "table" then
|
if typeof(signals) == "table" then
|
||||||
for _, signal in ipairs(signals) do
|
for _, signal in signals do
|
||||||
signal:Disconnect()
|
signal:Disconnect()
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -282,6 +282,22 @@ function PatchSet.assign(target, ...)
|
|||||||
return target
|
return target
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function PatchSet.addedIdList(patchSet): { string }
|
||||||
|
local idList = table.create(#patchSet.added)
|
||||||
|
for id in patchSet.added do
|
||||||
|
table.insert(idList, id)
|
||||||
|
end
|
||||||
|
return table.freeze(idList)
|
||||||
|
end
|
||||||
|
|
||||||
|
function PatchSet.updatedIdList(patchSet): { string }
|
||||||
|
local idList = table.create(#patchSet.updated)
|
||||||
|
for _, item in patchSet.updated do
|
||||||
|
table.insert(idList, item.id)
|
||||||
|
end
|
||||||
|
return table.freeze(idList)
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Create a list of human-readable statements summarizing the contents of this
|
Create a list of human-readable statements summarizing the contents of this
|
||||||
patch, intended to be displayed to users.
|
patch, intended to be displayed to users.
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ local Types = require(Plugin.Types)
|
|||||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
local getProperty = require(Plugin.Reconciler.getProperty)
|
||||||
|
|
||||||
|
local function yieldIfNeeded(clock)
|
||||||
|
if os.clock() - clock > 1 / 20 then
|
||||||
|
task.wait()
|
||||||
|
return os.clock()
|
||||||
|
end
|
||||||
|
return clock
|
||||||
|
end
|
||||||
|
|
||||||
local function alphabeticalNext(t, state)
|
local function alphabeticalNext(t, state)
|
||||||
-- Equivalent of the next function, but returns the keys in the alphabetic
|
-- 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
|
-- order of node names. We use a temporary ordered key table that is stored in the
|
||||||
@@ -132,7 +140,6 @@ end
|
|||||||
-- props must contain id, and cannot contain children or parentId
|
-- props must contain id, and cannot contain children or parentId
|
||||||
-- other than those three, it can hold anything
|
-- other than those three, it can hold anything
|
||||||
function Tree:addNode(parent, props)
|
function Tree:addNode(parent, props)
|
||||||
Timer.start("Tree:addNode")
|
|
||||||
assert(props.id, "props must contain id")
|
assert(props.id, "props must contain id")
|
||||||
|
|
||||||
parent = parent or "ROOT"
|
parent = parent or "ROOT"
|
||||||
@@ -143,7 +150,6 @@ function Tree:addNode(parent, props)
|
|||||||
for k, v in props do
|
for k, v in props do
|
||||||
node[k] = v
|
node[k] = v
|
||||||
end
|
end
|
||||||
Timer.stop()
|
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -154,25 +160,25 @@ function Tree:addNode(parent, props)
|
|||||||
local parentNode = self:getNode(parent)
|
local parentNode = self:getNode(parent)
|
||||||
if not parentNode then
|
if not parentNode then
|
||||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
||||||
Timer.stop()
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
parentNode.children[node.id] = node
|
parentNode.children[node.id] = node
|
||||||
self.idToNode[node.id] = node
|
self.idToNode[node.id] = node
|
||||||
|
|
||||||
Timer.stop()
|
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
||||||
-- using the patch and instanceMap info
|
-- using the patch and instanceMap info
|
||||||
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
|
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
|
||||||
Timer.start("Tree:buildAncestryNodes")
|
local clock = os.clock()
|
||||||
-- Build nodes for ancestry by going up the tree
|
-- Build nodes for ancestry by going up the tree
|
||||||
previousId = previousId or "ROOT"
|
previousId = previousId or "ROOT"
|
||||||
|
|
||||||
for _, ancestorId in ancestryIds do
|
for _, ancestorId in ancestryIds do
|
||||||
|
clock = yieldIfNeeded(clock)
|
||||||
|
|
||||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
||||||
if not value then
|
if not value then
|
||||||
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
||||||
@@ -186,8 +192,6 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
|
|||||||
})
|
})
|
||||||
previousId = ancestorId
|
previousId = ancestorId
|
||||||
end
|
end
|
||||||
|
|
||||||
Timer.stop()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local PatchTree = {}
|
local PatchTree = {}
|
||||||
@@ -196,12 +200,16 @@ local PatchTree = {}
|
|||||||
-- uses changeListHeaders in node.changeList
|
-- uses changeListHeaders in node.changeList
|
||||||
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
Timer.start("PatchTree.build")
|
Timer.start("PatchTree.build")
|
||||||
|
local clock = os.clock()
|
||||||
|
|
||||||
local tree = Tree.new()
|
local tree = Tree.new()
|
||||||
|
|
||||||
local knownAncestors = {}
|
local knownAncestors = {}
|
||||||
|
|
||||||
Timer.start("patch.updated")
|
Timer.start("patch.updated")
|
||||||
for _, change in patch.updated do
|
for _, change in patch.updated do
|
||||||
|
clock = yieldIfNeeded(clock)
|
||||||
|
|
||||||
local instance = instanceMap.fromIds[change.id]
|
local instance = instanceMap.fromIds[change.id]
|
||||||
if not instance then
|
if not instance then
|
||||||
continue
|
continue
|
||||||
@@ -281,6 +289,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||||||
|
|
||||||
Timer.start("patch.removed")
|
Timer.start("patch.removed")
|
||||||
for _, idOrInstance in patch.removed do
|
for _, idOrInstance in patch.removed do
|
||||||
|
clock = yieldIfNeeded(clock)
|
||||||
|
|
||||||
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
|
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
|
||||||
if not instance then
|
if not instance then
|
||||||
-- If we're viewing a past patch, the instance is already removed
|
-- If we're viewing a past patch, the instance is already removed
|
||||||
@@ -325,6 +335,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||||||
|
|
||||||
Timer.start("patch.added")
|
Timer.start("patch.added")
|
||||||
for id, change in patch.added do
|
for id, change in patch.added do
|
||||||
|
clock = yieldIfNeeded(clock)
|
||||||
|
|
||||||
-- Gather ancestors from existing DOM or future additions
|
-- Gather ancestors from existing DOM or future additions
|
||||||
local ancestryIds = {}
|
local ancestryIds = {}
|
||||||
local parentId = change.Parent
|
local parentId = change.Parent
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
Patches can come from the server or be generated by the client.
|
Patches can come from the server or be generated by the client.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
||||||
|
|
||||||
local Packages = script.Parent.Parent.Parent.Packages
|
local Packages = script.Parent.Parent.Parent.Packages
|
||||||
local Log = require(Packages.Log)
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
@@ -16,19 +14,18 @@ local invariant = require(script.Parent.Parent.invariant)
|
|||||||
|
|
||||||
local decodeValue = require(script.Parent.decodeValue)
|
local decodeValue = require(script.Parent.decodeValue)
|
||||||
local reify = require(script.Parent.reify)
|
local reify = require(script.Parent.reify)
|
||||||
|
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||||
local setProperty = require(script.Parent.setProperty)
|
local setProperty = require(script.Parent.setProperty)
|
||||||
|
|
||||||
local function applyPatch(instanceMap, patch)
|
local function applyPatch(instanceMap, patch)
|
||||||
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
|
|
||||||
local historyRecording = ChangeHistoryService:TryBeginRecording("Rojo: Patch " .. patchTimestamp)
|
|
||||||
if not historyRecording then
|
|
||||||
-- There can only be one recording at a time
|
|
||||||
Log.debug("Failed to begin history recording for " .. patchTimestamp .. ". Another recording is in progress.")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Tracks any portions of the patch that could not be applied to the DOM.
|
-- Tracks any portions of the patch that could not be applied to the DOM.
|
||||||
local unappliedPatch = PatchSet.newEmpty()
|
local unappliedPatch = PatchSet.newEmpty()
|
||||||
|
|
||||||
|
-- Contains a list of all of the ref properties that we'll need to assign.
|
||||||
|
-- It is imperative that refs are assigned after all instances are created
|
||||||
|
-- to ensure that referents can be mapped to instances correctly.
|
||||||
|
local deferredRefs = {}
|
||||||
|
|
||||||
for _, removedIdOrInstance in ipairs(patch.removed) do
|
for _, removedIdOrInstance in ipairs(patch.removed) do
|
||||||
local removeInstanceSuccess = pcall(function()
|
local removeInstanceSuccess = pcall(function()
|
||||||
if Types.RbxId(removedIdOrInstance) then
|
if Types.RbxId(removedIdOrInstance) then
|
||||||
@@ -67,9 +64,6 @@ local function applyPatch(instanceMap, patch)
|
|||||||
if parentInstance == nil then
|
if parentInstance == nil then
|
||||||
-- This would be peculiar. If you create an instance with no
|
-- This would be peculiar. If you create an instance with no
|
||||||
-- parent, were you supposed to create it at all?
|
-- parent, were you supposed to create it at all?
|
||||||
if historyRecording then
|
|
||||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
|
||||||
end
|
|
||||||
invariant(
|
invariant(
|
||||||
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
||||||
id,
|
id,
|
||||||
@@ -78,7 +72,7 @@ local function applyPatch(instanceMap, patch)
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
|
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
|
||||||
|
|
||||||
if not PatchSet.isEmpty(failedToReify) then
|
if not PatchSet.isEmpty(failedToReify) then
|
||||||
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
||||||
@@ -143,7 +137,7 @@ local function applyPatch(instanceMap, patch)
|
|||||||
[update.id] = mockVirtualInstance,
|
[update.id] = mockVirtualInstance,
|
||||||
}
|
}
|
||||||
|
|
||||||
local failedToReify = reify(instanceMap, mockAdded, update.id, instance.Parent)
|
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent)
|
||||||
|
|
||||||
local newInstance = instanceMap.fromIds[update.id]
|
local newInstance = instanceMap.fromIds[update.id]
|
||||||
|
|
||||||
@@ -206,6 +200,18 @@ local function applyPatch(instanceMap, patch)
|
|||||||
|
|
||||||
if update.changedProperties ~= nil then
|
if update.changedProperties ~= nil then
|
||||||
for propertyName, propertyValue in pairs(update.changedProperties) do
|
for propertyName, propertyValue in pairs(update.changedProperties) do
|
||||||
|
-- Because refs may refer to instances that we haven't constructed yet,
|
||||||
|
-- we defer applying any ref properties until all instances are created.
|
||||||
|
if next(propertyValue) == "Ref" then
|
||||||
|
table.insert(deferredRefs, {
|
||||||
|
id = update.id,
|
||||||
|
instance = instance,
|
||||||
|
propertyName = propertyName,
|
||||||
|
virtualValue = propertyValue,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
|
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
|
||||||
if not decodeSuccess then
|
if not decodeSuccess then
|
||||||
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
||||||
@@ -226,9 +232,7 @@ local function applyPatch(instanceMap, patch)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if historyRecording then
|
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
|
||||||
end
|
|
||||||
|
|
||||||
return unappliedPatch
|
return unappliedPatch
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,18 +25,26 @@ local function trueEquals(a, b): boolean
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Treat nil and { Ref = "000...0" } as equal
|
||||||
|
if
|
||||||
|
(a == nil and type(b) == "table" and b.Ref == "00000000000000000000000000000000")
|
||||||
|
or (b == nil and type(a) == "table" and a.Ref == "00000000000000000000000000000000")
|
||||||
|
then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local typeA, typeB = typeof(a), typeof(b)
|
local typeA, typeB = typeof(a), typeof(b)
|
||||||
|
|
||||||
-- For tables, try recursive deep equality
|
-- For tables, try recursive deep equality
|
||||||
if typeA == "table" and typeB == "table" then
|
if typeA == "table" and typeB == "table" then
|
||||||
local checkedKeys = {}
|
local checkedKeys = {}
|
||||||
for key, value in pairs(a) do
|
for key, value in a do
|
||||||
checkedKeys[key] = true
|
checkedKeys[key] = true
|
||||||
if not trueEquals(value, b[key]) then
|
if not trueEquals(value, b[key]) then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for key, value in pairs(b) do
|
for key, value in b do
|
||||||
if checkedKeys[key] then
|
if checkedKeys[key] then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
@@ -151,7 +159,24 @@ local function diff(instanceMap, virtualInstances, rootId)
|
|||||||
|
|
||||||
if getProperySuccess then
|
if getProperySuccess then
|
||||||
local existingValue = existingValueOrErr
|
local existingValue = existingValueOrErr
|
||||||
local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
|
local decodeSuccess, decodedValue
|
||||||
|
|
||||||
|
-- If `virtualValue` is a ref then instead of decoding it to an instance,
|
||||||
|
-- we change `existingValue` to be a ref. This is because `virtualValue`
|
||||||
|
-- may point to an Instance which doesn't exist yet and therefore
|
||||||
|
-- decoding it may throw an error.
|
||||||
|
if next(virtualValue) == "Ref" then
|
||||||
|
decodeSuccess, decodedValue = true, virtualValue
|
||||||
|
|
||||||
|
if existingValue and typeof(existingValue) == "Instance" then
|
||||||
|
local existingValueRef = instanceMap.fromInstances[existingValue]
|
||||||
|
if existingValueRef then
|
||||||
|
existingValue = { Ref = existingValueRef }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||||
|
end
|
||||||
|
|
||||||
if decodeSuccess then
|
if decodeSuccess then
|
||||||
if not trueEquals(existingValue, decodedValue) then
|
if not trueEquals(existingValue, decodedValue) then
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ return function()
|
|||||||
local function size(dict)
|
local function size(dict)
|
||||||
local len = 0
|
local len = 0
|
||||||
|
|
||||||
for _ in pairs(dict) do
|
for _ in dict do
|
||||||
len = len + 1
|
len = len + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
|
|||||||
for _, childId in ipairs(virtualInstance.Children) do
|
for _, childId in ipairs(virtualInstance.Children) do
|
||||||
local virtualChild = virtualInstances[childId]
|
local virtualChild = virtualInstances[childId]
|
||||||
|
|
||||||
for childIndex, childInstance in ipairs(existingChildren) do
|
for childIndex, childInstance in existingChildren do
|
||||||
if not isExistingChildVisited[childIndex] then
|
if not isExistingChildVisited[childIndex] then
|
||||||
-- We guard accessing Name and ClassName in order to avoid
|
-- We guard accessing Name and ClassName in order to avoid
|
||||||
-- tripping over children of DataModel that Rojo won't have
|
-- tripping over children of DataModel that Rojo won't have
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
local Packages = Rojo.Packages
|
|
||||||
|
|
||||||
local Log = require(Packages.Log)
|
|
||||||
|
|
||||||
local Timer = require(Plugin.Timer)
|
local Timer = require(Plugin.Timer)
|
||||||
|
|
||||||
@@ -22,78 +19,17 @@ function Reconciler.new(instanceMap)
|
|||||||
local self = {
|
local self = {
|
||||||
-- Tracks all of the instances known by the reconciler by ID.
|
-- Tracks all of the instances known by the reconciler by ID.
|
||||||
__instanceMap = instanceMap,
|
__instanceMap = instanceMap,
|
||||||
__precommitCallbacks = {},
|
|
||||||
__postcommitCallbacks = {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, Reconciler)
|
return setmetatable(self, Reconciler)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Reconciler:hookPrecommit(callback: (patch: any, instanceMap: any) -> ()): () -> ()
|
|
||||||
table.insert(self.__precommitCallbacks, callback)
|
|
||||||
Log.trace("Added precommit callback: {}", callback)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
-- Remove the callback from the list
|
|
||||||
for i, cb in self.__precommitCallbacks do
|
|
||||||
if cb == callback then
|
|
||||||
table.remove(self.__precommitCallbacks, i)
|
|
||||||
Log.trace("Removed precommit callback: {}", callback)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unappliedPatch: any) -> ()): () -> ()
|
|
||||||
table.insert(self.__postcommitCallbacks, callback)
|
|
||||||
Log.trace("Added postcommit callback: {}", callback)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
-- Remove the callback from the list
|
|
||||||
for i, cb in self.__postcommitCallbacks do
|
|
||||||
if cb == callback then
|
|
||||||
table.remove(self.__postcommitCallbacks, i)
|
|
||||||
Log.trace("Removed postcommit callback: {}", callback)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Reconciler:applyPatch(patch)
|
function Reconciler:applyPatch(patch)
|
||||||
Timer.start("Reconciler:applyPatch")
|
Timer.start("Reconciler:applyPatch")
|
||||||
|
|
||||||
Timer.start("precommitCallbacks")
|
|
||||||
-- Precommit callbacks must be serial in order to obey the contract that
|
|
||||||
-- they execute before commit
|
|
||||||
for _, callback in self.__precommitCallbacks do
|
|
||||||
local success, err = pcall(callback, patch, self.__instanceMap)
|
|
||||||
if not success then
|
|
||||||
Log.warn("Precommit hook errored: {}", err)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Timer.stop()
|
|
||||||
|
|
||||||
Timer.start("apply")
|
|
||||||
local unappliedPatch = applyPatch(self.__instanceMap, patch)
|
local unappliedPatch = applyPatch(self.__instanceMap, patch)
|
||||||
Timer.stop()
|
|
||||||
|
|
||||||
Timer.start("postcommitCallbacks")
|
|
||||||
-- Postcommit callbacks can be called with spawn since regardless of firing order, they are
|
|
||||||
-- guaranteed to be called after the commit
|
|
||||||
for _, callback in self.__postcommitCallbacks do
|
|
||||||
task.spawn(function()
|
|
||||||
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
|
|
||||||
if not success then
|
|
||||||
Log.warn("Postcommit hook errored: {}", err)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
Timer.stop()
|
|
||||||
|
|
||||||
Timer.stop()
|
Timer.stop()
|
||||||
|
|
||||||
return unappliedPatch
|
return unappliedPatch
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,26 +7,6 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
|
|||||||
local setProperty = require(script.Parent.setProperty)
|
local setProperty = require(script.Parent.setProperty)
|
||||||
local decodeValue = require(script.Parent.decodeValue)
|
local decodeValue = require(script.Parent.decodeValue)
|
||||||
|
|
||||||
local reifyInner, applyDeferredRefs
|
|
||||||
|
|
||||||
local function reify(instanceMap, virtualInstances, rootId, parentInstance)
|
|
||||||
-- Create an empty patch that will be populated with any parts of this reify
|
|
||||||
-- that could not happen, like instances that couldn't be created and
|
|
||||||
-- properties that could not be assigned.
|
|
||||||
local unappliedPatch = PatchSet.newEmpty()
|
|
||||||
|
|
||||||
-- Contains a list of all of the ref properties that we'll need to assign
|
|
||||||
-- after all instances are created. We apply refs in a second pass, after
|
|
||||||
-- we create as many instances as we can, so that we ensure that referents
|
|
||||||
-- can be mapped to instances correctly.
|
|
||||||
local deferredRefs = {}
|
|
||||||
|
|
||||||
reifyInner(instanceMap, virtualInstances, rootId, parentInstance, unappliedPatch, deferredRefs)
|
|
||||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|
||||||
|
|
||||||
return unappliedPatch
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Add the given ID and all of its descendants in virtualInstances to the given
|
Add the given ID and all of its descendants in virtualInstances to the given
|
||||||
PatchSet, marked for addition.
|
PatchSet, marked for addition.
|
||||||
@@ -40,10 +20,21 @@ local function addAllToPatch(patchSet, virtualInstances, id)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
||||||
|
-- Create an empty patch that will be populated with any parts of this reify
|
||||||
|
-- that could not happen, like instances that couldn't be created and
|
||||||
|
-- properties that could not be assigned.
|
||||||
|
local unappliedPatch = PatchSet.newEmpty()
|
||||||
|
|
||||||
|
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
||||||
|
|
||||||
|
return unappliedPatch
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Inner function that defines the core routine.
|
Inner function that defines the core routine.
|
||||||
]]
|
]]
|
||||||
function reifyInner(instanceMap, virtualInstances, id, parentInstance, unappliedPatch, deferredRefs)
|
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance)
|
||||||
local virtualInstance = virtualInstances[id]
|
local virtualInstance = virtualInstances[id]
|
||||||
|
|
||||||
if virtualInstance == nil then
|
if virtualInstance == nil then
|
||||||
@@ -102,7 +93,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
|
|||||||
end
|
end
|
||||||
|
|
||||||
for _, childId in ipairs(virtualInstance.Children) do
|
for _, childId in ipairs(virtualInstance.Children) do
|
||||||
reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
|
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
|
||||||
end
|
end
|
||||||
|
|
||||||
instance.Parent = parentInstance
|
instance.Parent = parentInstance
|
||||||
@@ -135,7 +126,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, entry in ipairs(deferredRefs) do
|
for _, entry in deferredRefs do
|
||||||
local _, refId = next(entry.virtualValue)
|
local _, refId = next(entry.virtualValue)
|
||||||
|
|
||||||
if refId == nil then
|
if refId == nil then
|
||||||
@@ -143,6 +134,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local targetInstance = instanceMap.fromIds[refId]
|
local targetInstance = instanceMap.fromIds[refId]
|
||||||
|
|
||||||
if targetInstance == nil then
|
if targetInstance == nil then
|
||||||
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||||
continue
|
continue
|
||||||
@@ -155,4 +147,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return reify
|
return {
|
||||||
|
reifyInstance = reifyInstance,
|
||||||
|
applyDeferredRefs = applyDeferredRefs,
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user