Compare commits
132 Commits
v7.3.0
...
aarch-wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6688bcb488 | ||
|
|
73097075d4 | ||
|
|
5e1cab2e75 | ||
|
|
30f439caec | ||
|
|
4b5db4e5a9 | ||
|
|
3fa1d6b09c | ||
|
|
6051a5f1f1 | ||
|
|
5f7dd45361 | ||
|
|
3ca975d81d | ||
|
|
7e2bab921a | ||
|
|
a7b45ee859 | ||
|
|
62f4a1f3c2 | ||
|
|
3d4e387d35 | ||
|
|
2c46640105 | ||
|
|
41443d3989 | ||
|
|
4b3470d30b | ||
|
|
ce71a3df4d | ||
|
|
7232721b87 | ||
|
|
b2f133e6f1 | ||
|
|
87920964d7 | ||
|
|
c7a4f892e3 | ||
|
|
8f9e307930 | ||
|
|
856d43ce69 | ||
|
|
26181a5a1f | ||
|
|
edf87bf9a3 | ||
|
|
5f51538e0b | ||
|
|
48bb760739 | ||
|
|
42121a9fc9 | ||
|
|
02d79a4749 | ||
|
|
ddb26c73bd | ||
|
|
8ff064fe28 | ||
|
|
cf25eb0833 | ||
|
|
5c4260f3ac | ||
|
|
7abf19804c | ||
|
|
df707d5bef | ||
|
|
f3b0b0027e | ||
|
|
106a01223e | ||
|
|
506a60d0be | ||
|
|
4018607b77 | ||
|
|
1cc720ad34 | ||
|
|
73828af715 | ||
|
|
c0a96e3811 | ||
|
|
9d0d76f0a5 | ||
|
|
c7173ac832 | ||
|
|
b12ce47e7e | ||
|
|
269272983b | ||
|
|
6adc5eb9fb | ||
|
|
fd8bc8ae3f | ||
|
|
3369b0d429 | ||
|
|
097d39e8ce | ||
|
|
11fa08e6d6 | ||
|
|
96987af71d | ||
|
|
23327cb3ef | ||
|
|
b43b45be8f | ||
|
|
41994ec82e | ||
|
|
cd14ea7c62 | ||
|
|
9f13bca6b8 | ||
|
|
f4252c3e97 | ||
|
|
6598867d3d | ||
|
|
f39e040a0d | ||
|
|
a3d140269b | ||
|
|
feac29ea40 | ||
|
|
834c8cdbca | ||
|
|
d441fbdf91 | ||
|
|
e897f524dc | ||
|
|
1caf9446d8 | ||
|
|
bfd2c885db | ||
|
|
f467fa4e59 | ||
|
|
41fca4a2bb | ||
|
|
d38f955144 | ||
|
|
010e50a25d | ||
|
|
eab7c607cd | ||
|
|
3cafbf7f1a | ||
|
|
d7277b5a5b | ||
|
|
bb8dd1402d | ||
|
|
539cd0d418 | ||
|
|
0f8e1625d5 | ||
|
|
840e9bedb2 | ||
|
|
e11ad476fc | ||
|
|
c43726bc75 | ||
|
|
c9ab933a23 | ||
|
|
066a0b1668 | ||
|
|
aa68fe412e | ||
|
|
d748ea7e40 | ||
|
|
a7a282078f | ||
|
|
2fad3b588a | ||
|
|
4cb5d4a9c5 | ||
|
|
5b22ef192e | ||
|
|
34024d8524 | ||
|
|
ecc31dea15 | ||
|
|
d0e48d9bdc | ||
|
|
f6fc5599c0 | ||
|
|
89b6666436 | ||
|
|
94d45a2262 | ||
|
|
dc17a185ca | ||
|
|
4915477823 | ||
|
|
8662d2227c | ||
|
|
dd01a9bef3 | ||
|
|
6e320b1fd5 | ||
|
|
6e40993199 | ||
|
|
9d48af2b50 | ||
|
|
28d48a76e3 | ||
|
|
80eb14f9da | ||
|
|
623fa06d52 | ||
|
|
7154113c13 | ||
|
|
0a932ff880 | ||
|
|
7ef4a1ff12 | ||
|
|
ccc52b69d2 | ||
|
|
8139fdc738 | ||
|
|
a4fd53d516 | ||
|
|
27357110b5 | ||
|
|
fde78738b6 | ||
|
|
ce530e795a | ||
|
|
658d211779 | ||
|
|
66c1cd0d93 | ||
|
|
55ac231cec | ||
|
|
67674d53a2 | ||
|
|
8646b2dfce | ||
|
|
a2f68c2e3c | ||
|
|
5b1a090c5e | ||
|
|
e9efa238b0 | ||
|
|
0dabd8a1f6 | ||
|
|
b7a1f82f56 | ||
|
|
2507e096b7 | ||
|
|
b303b0a99c | ||
|
|
342fb57d14 | ||
|
|
a9ca77e27f | ||
|
|
6542304340 | ||
|
|
6b0f7f94b6 | ||
|
|
d87c76a23e | ||
|
|
305423b856 | ||
|
|
4b62190aff |
@@ -24,3 +24,6 @@ insert_final_newline = true
|
|||||||
|
|
||||||
[*.lua]
|
[*.lua]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.luau]
|
||||||
|
indent_style = tab
|
||||||
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# stylua formatting
|
||||||
|
0f8e1625d572a5fe0f7b5c08653ff92cc837d346
|
||||||
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.lua linguist-language=Luau
|
||||||
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
patreon: lpghatguy
|
|
||||||
23
.github/workflows/changelog.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Changelog Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Check Actions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Changelog check
|
||||||
|
uses: Zomzog/changelog-checker@v1.3.0
|
||||||
|
with:
|
||||||
|
fileName: CHANGELOG.md
|
||||||
|
noChangelogLabel: skip changelog
|
||||||
|
checkNotification: Simple
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
76
.github/workflows/ci.yml
vendored
@@ -12,66 +12,88 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Test
|
name: Build and Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust_version: [stable, 1.69.0]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust_version }}
|
- name: Rust cache
|
||||||
override: true
|
uses: Swatinem/rust-cache@v2
|
||||||
profile: minimal
|
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Setup Aftman
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
uses: ok-nick/setup-aftman@v0.3.0
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
version: 'v0.2.7'
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
cd plugin
|
|
||||||
wally install
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
run: cargo build --locked --verbose
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --locked --verbose
|
run: cargo test --locked --verbose
|
||||||
|
|
||||||
lint:
|
msrv:
|
||||||
name: Rustfmt and Clippy
|
name: Check MSRV
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@1.70.0
|
||||||
with:
|
|
||||||
toolchain: stable
|
- name: Rust cache
|
||||||
override: true
|
uses: Swatinem/rust-cache@v2
|
||||||
components: rustfmt, clippy
|
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Setup Aftman
|
||||||
uses: ok-nick/setup-aftman@v0.3.0
|
uses: ok-nick/setup-aftman@v0.3.0
|
||||||
with:
|
with:
|
||||||
version: 'v0.2.7'
|
version: 'v0.2.7'
|
||||||
|
|
||||||
- name: Install packages
|
- name: Build
|
||||||
run: |
|
run: cargo build --locked --verbose
|
||||||
cd plugin
|
|
||||||
wally install
|
lint:
|
||||||
cd ..
|
name: Rustfmt, Clippy, Stylua, & Selene
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Setup Aftman
|
||||||
|
uses: ok-nick/setup-aftman@v0.3.0
|
||||||
|
with:
|
||||||
|
version: 'v0.2.7'
|
||||||
|
|
||||||
|
- name: Stylua
|
||||||
|
run: stylua --check plugin/src
|
||||||
|
|
||||||
|
- name: Selene
|
||||||
|
run: selene plugin/src
|
||||||
|
|
||||||
- name: Rustfmt
|
- name: Rustfmt
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy
|
run: cargo clippy
|
||||||
|
|
||||||
|
|||||||
108
.github/workflows/release.yml
vendored
@@ -8,26 +8,22 @@ jobs:
|
|||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
tag_name: ${{ github.ref }}
|
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
|
||||||
release_name: ${{ github.ref }}
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
build-plugin:
|
build-plugin:
|
||||||
needs: ["create-release"]
|
needs: ["create-release"]
|
||||||
name: Build Roblox Studio Plugin
|
name: Build Roblox Studio Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Setup Aftman
|
||||||
uses: ok-nick/setup-aftman@v0.1.0
|
uses: ok-nick/setup-aftman@v0.1.0
|
||||||
@@ -36,27 +32,17 @@ jobs:
|
|||||||
trust-check: false
|
trust-check: false
|
||||||
version: 'v0.2.6'
|
version: 'v0.2.6'
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
cd plugin
|
|
||||||
wally install
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Build Plugin
|
- name: Build Plugin
|
||||||
run: rojo build plugin --output Rojo.rbxm
|
run: rojo build plugin --output Rojo.rbxm
|
||||||
|
|
||||||
- name: Upload Plugin to Release
|
- name: Upload Plugin to Release
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
gh release upload ${{ github.ref_name }} Rojo.rbxm
|
||||||
asset_path: Rojo.rbxm
|
|
||||||
asset_name: Rojo.rbxm
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|
||||||
- name: Upload Plugin to Artifacts
|
- name: Upload Plugin to Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Rojo.rbxm
|
name: Rojo.rbxm
|
||||||
path: Rojo.rbxm
|
path: Rojo.rbxm
|
||||||
@@ -73,11 +59,21 @@ jobs:
|
|||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
label: linux-x86_64
|
label: linux-x86_64
|
||||||
|
|
||||||
|
- host: linux
|
||||||
|
os: ubuntu-20.04
|
||||||
|
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-latest
|
||||||
|
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
|
||||||
@@ -93,22 +89,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BIN: rojo
|
BIN: rojo
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
- name: Get Version from Tag
|
submodules: true
|
||||||
shell: bash
|
|
||||||
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
|
|
||||||
run: |
|
|
||||||
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
|
||||||
echo "Version is: ${{ env.PROJECT_VERSION }}"
|
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
targets: ${{ matrix.target }}
|
||||||
target: ${{ matrix.target }}
|
|
||||||
override: true
|
|
||||||
profile: minimal
|
|
||||||
|
|
||||||
- name: Setup Aftman
|
- name: Setup Aftman
|
||||||
uses: ok-nick/setup-aftman@v0.1.0
|
uses: ok-nick/setup-aftman@v0.1.0
|
||||||
@@ -117,51 +105,41 @@ jobs:
|
|||||||
trust-check: false
|
trust-check: false
|
||||||
version: 'v0.2.6'
|
version: 'v0.2.6'
|
||||||
|
|
||||||
- name: Install packages
|
|
||||||
run: |
|
|
||||||
cd plugin
|
|
||||||
wally install
|
|
||||||
cd ..
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: cargo build --release --locked --verbose
|
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
# Build into a known directory so we can find our build artifact more
|
# Build into a known directory so we can find our build artifact more
|
||||||
# easily.
|
# easily.
|
||||||
CARGO_TARGET_DIR: output
|
CARGO_TARGET_DIR: output
|
||||||
|
|
||||||
# On platforms that use OpenSSL, ensure it is statically linked to
|
- name: Generate Artifact Name
|
||||||
# make binaries more portable.
|
|
||||||
OPENSSL_STATIC: 1
|
|
||||||
|
|
||||||
- name: Create Release Archive
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Create Archive and Upload to Release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
mkdir staging
|
mkdir staging
|
||||||
|
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
cp "output/release/$BIN.exe" staging/
|
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||||
cd staging
|
cd staging
|
||||||
7z a ../release.zip *
|
7z a ../$ARTIFACT_NAME *
|
||||||
else
|
else
|
||||||
cp "output/release/$BIN" staging/
|
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
||||||
cd staging
|
cd staging
|
||||||
zip ../release.zip *
|
zip ../$ARTIFACT_NAME *
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Archive to Release
|
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: release.zip
|
|
||||||
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|
||||||
- name: Upload Archive to Artifacts
|
- name: Upload Archive to Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
path: ${{ env.ARTIFACT_NAME }}
|
||||||
path: release.zip
|
name: ${{ env.ARTIFACT_NAME }}
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -13,9 +13,6 @@
|
|||||||
# Test places for the Roblox Studio Plugin
|
# Test places for the Roblox Studio Plugin
|
||||||
/plugin/*.rbxlx
|
/plugin/*.rbxlx
|
||||||
|
|
||||||
# Packages for the Roblox Studio Plugin
|
|
||||||
/plugin/*Packages
|
|
||||||
|
|
||||||
# Roblox Studio holds 'lock' files on places
|
# Roblox Studio holds 'lock' files on places
|
||||||
*.rbxl.lock
|
*.rbxl.lock
|
||||||
*.rbxlx.lock
|
*.rbxlx.lock
|
||||||
|
|||||||
18
.gitmodules
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[submodule "plugin/Packages/Roact"]
|
||||||
|
path = plugin/Packages/Roact
|
||||||
|
url = https://github.com/roblox/roact.git
|
||||||
|
[submodule "plugin/Packages/Flipper"]
|
||||||
|
path = plugin/Packages/Flipper
|
||||||
|
url = https://github.com/reselim/flipper.git
|
||||||
|
[submodule "plugin/Packages/Promise"]
|
||||||
|
path = plugin/Packages/Promise
|
||||||
|
url = https://github.com/evaera/roblox-lua-promise.git
|
||||||
|
[submodule "plugin/Packages/t"]
|
||||||
|
path = plugin/Packages/t
|
||||||
|
url = https://github.com/osyrisrblx/t.git
|
||||||
|
[submodule "plugin/Packages/TestEZ"]
|
||||||
|
path = plugin/Packages/TestEZ
|
||||||
|
url = https://github.com/roblox/testez.git
|
||||||
|
[submodule "plugin/Packages/Highlighter"]
|
||||||
|
path = plugin/Packages/Highlighter
|
||||||
|
url = https://github.com/boatbomber/highlighter.git
|
||||||
307
CHANGELOG.md
@@ -1,6 +1,313 @@
|
|||||||
# Rojo Changelog
|
# Rojo Changelog
|
||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
|
||||||
|
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
|
||||||
|
is given an ID. Then, that ID may be used elsewhere in the project to point to an Instance
|
||||||
|
using an attribute named `Rojo_Target_PROP_NAME`, where `PROP_NAME` is the name of a property.
|
||||||
|
|
||||||
|
As an example, here is a `model.json` for an ObjectValue that refers to itself:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "arbitrary string",
|
||||||
|
"attributes": {
|
||||||
|
"Rojo_Target_Value": "arbitrary string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a very rough implementation and the usage will become more ergonomic
|
||||||
|
over time.
|
||||||
|
|
||||||
|
* Updated Undo/Redo history to be more robust ([#915])
|
||||||
|
* Added popout diff visualizer for table properties like Attributes and Tags ([#834])
|
||||||
|
* Updated Theme to use Studio colors ([#838])
|
||||||
|
* Improved patch visualizer UX ([#883])
|
||||||
|
* Added update notifications for newer compatible versions in the Studio plugin. ([#832])
|
||||||
|
* Added experimental setting for Auto Connect in playtests ([#840])
|
||||||
|
* Improved settings UI ([#886])
|
||||||
|
* `Open Scripts Externally` option can now be changed while syncing ([#911])
|
||||||
|
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
|
||||||
|
This is specified via a new field on project files, `syncRules`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"syncRules": [
|
||||||
|
{
|
||||||
|
"pattern": "*.foo",
|
||||||
|
"use": "text",
|
||||||
|
"exclude": "*.exclude.foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "*.bar.baz",
|
||||||
|
"use": "json",
|
||||||
|
"suffix": ".bar.baz",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "SyncRulesAreCool",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `pattern` field is a glob used to match the sync rule to files. If present, the `suffix` field allows you to specify parts of a file's name get cut off by Rojo to name the Instance, including the file extension. If it isn't specified, Rojo will only cut off the first part of the file extension, up to the first dot.
|
||||||
|
|
||||||
|
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
|
||||||
|
|
||||||
|
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type. A full list is below:
|
||||||
|
|
||||||
|
| `use` value | file extension |
|
||||||
|
|:---------------|:----------------|
|
||||||
|
| `serverScript` | `.server.lua` |
|
||||||
|
| `clientScript` | `.client.lua` |
|
||||||
|
| `moduleScript` | `.lua` |
|
||||||
|
| `json` | `.json` |
|
||||||
|
| `toml` | `.toml` |
|
||||||
|
| `csv` | `.csv` |
|
||||||
|
| `text` | `.txt` |
|
||||||
|
| `jsonModel` | `.model.json` |
|
||||||
|
| `rbxm` | `.rbxm` |
|
||||||
|
| `rbxmx` | `.rbxmx` |
|
||||||
|
| `project` | `.project.json` |
|
||||||
|
| `ignore` | None! |
|
||||||
|
|
||||||
|
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
|
||||||
|
|
||||||
|
[#813]: https://github.com/rojo-rbx/rojo/pull/813
|
||||||
|
[#832]: https://github.com/rojo-rbx/rojo/pull/832
|
||||||
|
[#834]: https://github.com/rojo-rbx/rojo/pull/834
|
||||||
|
[#838]: https://github.com/rojo-rbx/rojo/pull/838
|
||||||
|
[#840]: https://github.com/rojo-rbx/rojo/pull/840
|
||||||
|
[#843]: https://github.com/rojo-rbx/rojo/pull/843
|
||||||
|
[#883]: https://github.com/rojo-rbx/rojo/pull/883
|
||||||
|
[#886]: https://github.com/rojo-rbx/rojo/pull/886
|
||||||
|
[#911]: https://github.com/rojo-rbx/rojo/pull/911
|
||||||
|
[#915]: https://github.com/rojo-rbx/rojo/pull/915
|
||||||
|
|
||||||
|
## [7.4.3] - August 6th, 2024
|
||||||
|
* Fixed issue with building binary files introduced in 7.4.2
|
||||||
|
* Fixed `value of type nil cannot be converted to number` warning spam in output. [#955]
|
||||||
|
|
||||||
|
[#955]: https://github.com/rojo-rbx/rojo/pull/955
|
||||||
|
|
||||||
|
## [7.4.2] - July 23, 2024
|
||||||
|
* Added Never option to Confirmation ([#893])
|
||||||
|
* Fixed removing trailing newlines ([#903])
|
||||||
|
* Updated the internal property database, correcting an issue with `SurfaceAppearance.Color` that was reported [here][Surface_Appearance_Color_1] and [here][Surface_Appearance_Color_2] ([#948])
|
||||||
|
|
||||||
|
[#893]: https://github.com/rojo-rbx/rojo/pull/893
|
||||||
|
[#903]: https://github.com/rojo-rbx/rojo/pull/903
|
||||||
|
[#948]: https://github.com/rojo-rbx/rojo/pull/948
|
||||||
|
[Surface_Appearance_Color_1]: https://devforum.roblox.com/t/jailbreak-custom-character-turned-shiny-black-no-texture/3075563
|
||||||
|
[Surface_Appearance_Color_2]: https://devforum.roblox.com/t/surfaceappearance-not-displaying-correctly/3075588
|
||||||
|
|
||||||
|
## [7.4.1] - February 20, 2024
|
||||||
|
* Made the `name` field optional on project files ([#870])
|
||||||
|
|
||||||
|
Files named `default.project.json` inherit the name of the folder they're in and all other projects
|
||||||
|
are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`)
|
||||||
|
|
||||||
|
There is no change in behavior if `name` is set.
|
||||||
|
* Fixed incorrect results when building model pivots ([#865])
|
||||||
|
* Fixed incorrect results when serving model pivots ([#868])
|
||||||
|
* Rojo now converts any line endings to LF, preventing spurious diffs when syncing Lua files on Windows ([#854])
|
||||||
|
* Fixed Rojo plugin failing to connect when project contains certain unreadable properties ([#848])
|
||||||
|
* Fixed various cases where patch visualizer would not display sync failures ([#845], [#844])
|
||||||
|
* Fixed http error handling so Rojo can be used in Github Codespaces ([#847])
|
||||||
|
|
||||||
|
[#848]: https://github.com/rojo-rbx/rojo/pull/848
|
||||||
|
[#845]: https://github.com/rojo-rbx/rojo/pull/845
|
||||||
|
[#844]: https://github.com/rojo-rbx/rojo/pull/844
|
||||||
|
[#847]: https://github.com/rojo-rbx/rojo/pull/847
|
||||||
|
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||||
|
[#865]: https://github.com/rojo-rbx/rojo/pull/865
|
||||||
|
[#868]: https://github.com/rojo-rbx/rojo/pull/868
|
||||||
|
[#870]: https://github.com/rojo-rbx/rojo/pull/870
|
||||||
|
|
||||||
|
## [7.4.0] - January 16, 2024
|
||||||
|
* Improved the visualization for array properties like Tags ([#829])
|
||||||
|
* Significantly improved performance of `rojo serve`, `rojo build --watch`, and `rojo sourcemap --watch` on macOS. ([#830])
|
||||||
|
* Changed *.lua files that init command generates to *.luau ([#831])
|
||||||
|
* Does not remind users to sync if the sync lock is claimed already ([#833])
|
||||||
|
|
||||||
|
[#829]: https://github.com/rojo-rbx/rojo/pull/829
|
||||||
|
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||||
|
[#831]: https://github.com/rojo-rbx/rojo/pull/831
|
||||||
|
[#833]: https://github.com/rojo-rbx/rojo/pull/833
|
||||||
|
|
||||||
|
## [7.4.0-rc3] - October 25, 2023
|
||||||
|
* Changed `sourcemap --watch` to only generate the sourcemap when it's necessary ([#800])
|
||||||
|
* Switched script source property getter and setter to `ScriptEditorService` methods ([#801])
|
||||||
|
|
||||||
|
This ensures that the script editor reflects any changes Rojo makes to a script while it is open in the script editor.
|
||||||
|
|
||||||
|
* Fixed issues when handling `SecurityCapabilities` values ([#803], [#807])
|
||||||
|
* Fixed Rojo plugin erroring out when attempting to sync attributes with invalid names ([#809])
|
||||||
|
|
||||||
|
[#800]: https://github.com/rojo-rbx/rojo/pull/800
|
||||||
|
[#801]: https://github.com/rojo-rbx/rojo/pull/801
|
||||||
|
[#803]: https://github.com/rojo-rbx/rojo/pull/803
|
||||||
|
[#807]: https://github.com/rojo-rbx/rojo/pull/807
|
||||||
|
[#809]: https://github.com/rojo-rbx/rojo/pull/809
|
||||||
|
|
||||||
|
## [7.4.0-rc2] - October 3, 2023
|
||||||
|
* Fixed bug with parsing version for plugin validation ([#797])
|
||||||
|
|
||||||
|
[#797]: https://github.com/rojo-rbx/rojo/pull/797
|
||||||
|
|
||||||
|
## [7.4.0-rc1] - October 3, 2023
|
||||||
|
### Additions
|
||||||
|
#### Project format
|
||||||
|
* Added support for `.toml` files to `$path` ([#633])
|
||||||
|
* Added support for `Font` and `CFrame` attributes ([rbx-dom#299], [rbx-dom#296])
|
||||||
|
* Added the `emitLegacyScripts` field to the project format ([#765]). The behavior is outlined below:
|
||||||
|
|
||||||
|
| `emitLegacyScripts` Value | Action Taken by Rojo |
|
||||||
|
|---------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| false | Rojo emits Scripts with the appropriate `RunContext` for `*.client.lua` and `*.server.lua` files in the project. |
|
||||||
|
| true (default) | Rojo emits LocalScripts and Scripts with legacy `RunContext` (same behavior as previously). |
|
||||||
|
|
||||||
|
|
||||||
|
It can be used like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"emitLegacyScripts": false,
|
||||||
|
"name": "MyCoolRunContextProject",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Added `Terrain` classname inference, similar to services ([#771])
|
||||||
|
|
||||||
|
`Terrain` may now be defined in projects without using `$className`:
|
||||||
|
```json
|
||||||
|
"Workspace": {
|
||||||
|
"Terrain": {
|
||||||
|
"$path": "path/to/terrain.rbxm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Added support for `Terrain.MaterialColors` ([#770])
|
||||||
|
|
||||||
|
`Terrain.MaterialColors` is now represented in projects in a human readable format:
|
||||||
|
```json
|
||||||
|
"Workspace": {
|
||||||
|
"Terrain": {
|
||||||
|
"$path": "path/to/terrain.rbxm"
|
||||||
|
"$properties": {
|
||||||
|
"MaterialColors": {
|
||||||
|
"Grass": [10, 20, 30],
|
||||||
|
"Asphalt": [40, 50, 60],
|
||||||
|
"LeafyGrass": [255, 155, 55]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Added better support for `Font` properties ([#731])
|
||||||
|
|
||||||
|
`FontFace` properties may now be defined using implicit property syntax:
|
||||||
|
```json
|
||||||
|
"TextBox": {
|
||||||
|
"$className": "TextBox",
|
||||||
|
"$properties": {
|
||||||
|
"FontFace": {
|
||||||
|
"family": "rbxasset://fonts/families/RobotoMono.json",
|
||||||
|
"weight": "Thin",
|
||||||
|
"style": "Normal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Patch visualizer and notifications
|
||||||
|
* Added a setting to control patch confirmation behavior ([#774])
|
||||||
|
|
||||||
|
This is a new setting for controlling when the Rojo plugin prompts for confirmation before syncing. It has four options:
|
||||||
|
* Initial (default): prompts only once for a project in a given Studio session
|
||||||
|
* Always: always prompts for confirmation
|
||||||
|
* Large Changes: only prompts when there are more than X changed instances. The number of instances is configurable - an additional setting for the number of instances becomes available when this option is chosen
|
||||||
|
* Unlisted PlaceId: only prompts if the place ID is not present in servePlaceIds
|
||||||
|
|
||||||
|
* Added the ability to select Instances in patch visualizer ([#709])
|
||||||
|
|
||||||
|
Double-clicking an instance in the patch visualizer sets Roblox Studio's selection to the instance.
|
||||||
|
|
||||||
|
* Added a sync reminder notification. ([#689])
|
||||||
|
|
||||||
|
Rojo detects if you have previously synced to a place, and displays a notification reminding you to sync again:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* Added rich Source diffs in patch visualizer ([#748])
|
||||||
|
|
||||||
|
A "View Diff" button for script sources is now present in the patch visualizer. Clicking it displays a side-by-side diff of the script changes:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* Patch visualizer now indicates what changes failed to apply. ([#717])
|
||||||
|
|
||||||
|
A clickable warning label is displayed when the Rojo plugin is unable to apply changes. Clicking the label displays precise information about which changes failed:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
#### Miscellaneous
|
||||||
|
* Added `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
||||||
|
|
||||||
|
This is a flag that builds a Rojo project into Roblox Studio's plugins directory. This allows you to build a Rojo project and load it into Studio as a plugin without having to type the full path to the plugins directory. It can be used like this: `rojo build <PATH-TO-PROJECT> --plugin <FILE-NAME>`
|
||||||
|
|
||||||
|
* Added new plugin template to the `init` command ([#738])
|
||||||
|
|
||||||
|
This is a new template geared towards plugins. It is similar to the model template, but creates a `Script` instead of a `ModuleScript` in the `src` directory. It can be used like this: `rojo init --kind plugin`
|
||||||
|
|
||||||
|
* Added protection against syncing non-place projects as a place. ([#691])
|
||||||
|
* Add buttons for navigation on the Connected page ([#722])
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
* Significantly improved performance of `rojo sourcemap` ([#668])
|
||||||
|
* Fixed the diff visualizer of connected sessions. ([#674])
|
||||||
|
* Fixed disconnected session activity. ([#675])
|
||||||
|
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
||||||
|
* Fix Rojo breaking when users undo/redo in Studio ([#708])
|
||||||
|
* Improve tooltip behavior ([#723])
|
||||||
|
* Better settings controls ([#725])
|
||||||
|
* Rework patch visualizer with many fixes and improvements ([#713], [#726], [#755])
|
||||||
|
|
||||||
|
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||||
|
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
||||||
|
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
||||||
|
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
||||||
|
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
||||||
|
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
||||||
|
[#709]: https://github.com/rojo-rbx/rojo/pull/709
|
||||||
|
[#708]: https://github.com/rojo-rbx/rojo/pull/708
|
||||||
|
[#713]: https://github.com/rojo-rbx/rojo/pull/713
|
||||||
|
[#717]: https://github.com/rojo-rbx/rojo/pull/717
|
||||||
|
[#722]: https://github.com/rojo-rbx/rojo/pull/722
|
||||||
|
[#723]: https://github.com/rojo-rbx/rojo/pull/723
|
||||||
|
[#725]: https://github.com/rojo-rbx/rojo/pull/725
|
||||||
|
[#726]: https://github.com/rojo-rbx/rojo/pull/726
|
||||||
|
[#633]: https://github.com/rojo-rbx/rojo/pull/633
|
||||||
|
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
||||||
|
[#731]: https://github.com/rojo-rbx/rojo/pull/731
|
||||||
|
[#738]: https://github.com/rojo-rbx/rojo/pull/738
|
||||||
|
[#748]: https://github.com/rojo-rbx/rojo/pull/748
|
||||||
|
[#755]: https://github.com/rojo-rbx/rojo/pull/755
|
||||||
|
[#765]: https://github.com/rojo-rbx/rojo/pull/765
|
||||||
|
[#770]: https://github.com/rojo-rbx/rojo/pull/770
|
||||||
|
[#771]: https://github.com/rojo-rbx/rojo/pull/771
|
||||||
|
[#774]: https://github.com/rojo-rbx/rojo/pull/774
|
||||||
|
[rbx-dom#299]: https://github.com/rojo-rbx/rbx-dom/pull/299
|
||||||
|
[rbx-dom#296]: https://github.com/rojo-rbx/rbx-dom/pull/296
|
||||||
|
|
||||||
## [7.3.0] - April 22, 2023
|
## [7.3.0] - April 22, 2023
|
||||||
* Added `$attributes` to project format. ([#574])
|
* Added `$attributes` to project format. ([#574])
|
||||||
|
|||||||
1355
Cargo.lock
generated
96
Cargo.toml
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.3.0"
|
version = "7.4.0"
|
||||||
rust-version = "1.68.2"
|
rust-version = "1.70.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.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"
|
||||||
@@ -12,9 +12,7 @@ readme = "README.md"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
exclude = [
|
exclude = ["/test-projects/**"]
|
||||||
"/test-projects/**",
|
|
||||||
]
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
@@ -28,7 +26,9 @@ default = []
|
|||||||
# Enable this feature to live-reload assets from the web UI.
|
# Enable this feature to live-reload assets from the web UI.
|
||||||
dev_live_assets = []
|
dev_live_assets = []
|
||||||
|
|
||||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
|
# Run Rojo with this feature to open a Tracy session.
|
||||||
|
# Currently uses protocol v63, last supported in Tracy 0.9.1.
|
||||||
|
profile-with-tracy = ["profiling/profile-with-tracy"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
@@ -42,7 +42,7 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
memofs = { version = "0.3.0", 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" }
|
||||||
@@ -51,60 +51,66 @@ memofs = { version = "0.2.0", path = "crates/memofs" }
|
|||||||
# 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.0"
|
rbx_binary = "0.7.7"
|
||||||
rbx_dom_weak = "2.4.0"
|
rbx_dom_weak = "2.9.0"
|
||||||
rbx_reflection = "4.2.0"
|
rbx_reflection = "4.7.0"
|
||||||
rbx_reflection_database = "0.2.6"
|
rbx_reflection_database = "0.2.12"
|
||||||
rbx_xml = "0.13.0"
|
rbx_xml = "0.13.5"
|
||||||
|
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.80"
|
||||||
backtrace = "0.3.61"
|
backtrace = "0.3.69"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
crossbeam-channel = "0.5.1"
|
crossbeam-channel = "0.5.12"
|
||||||
csv = "1.1.6"
|
csv = "1.3.0"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.3"
|
||||||
fs-err = "2.6.0"
|
fs-err = "2.11.0"
|
||||||
futures = "0.3.17"
|
futures = "0.3.30"
|
||||||
globset = "0.4.8"
|
globset = "0.4.14"
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
|
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||||
jod-thread = "0.1.2"
|
jod-thread = "0.1.2"
|
||||||
log = "0.4.14"
|
log = "0.4.21"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
notify = "4.0.17"
|
num_cpus = "1.16.0"
|
||||||
opener = "0.5.0"
|
opener = "0.5.2"
|
||||||
reqwest = { version = "0.11.10", features = ["blocking", "json", "native-tls-vendored"] }
|
rayon = "1.9.0"
|
||||||
|
reqwest = { version = "0.11.24", default-features = false, features = [
|
||||||
|
"blocking",
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
roblox_install = "1.0.0"
|
roblox_install = "1.0.0"
|
||||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.68"
|
serde_json = "1.0.114"
|
||||||
termcolor = "1.1.2"
|
toml = "0.5.11"
|
||||||
thiserror = "1.0.30"
|
termcolor = "1.4.1"
|
||||||
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
thiserror = "1.0.57"
|
||||||
uuid = { version = "1.0.0", features = ["v4", "serde"] }
|
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||||
clap = { version = "3.1.18", features = ["derive"] }
|
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||||
profiling = "1.0.6"
|
clap = { version = "3.2.25", features = ["derive"] }
|
||||||
tracy-client = { version = "0.13.2", optional = true }
|
profiling = "1.0.15"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||||
|
|
||||||
embed-resource = "1.6.4"
|
embed-resource = "1.8.0"
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.80"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
fs-err = "2.6.0"
|
fs-err = "2.11.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
|
semver = "1.0.22"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||||
|
|
||||||
criterion = "0.3.5"
|
criterion = "0.3.6"
|
||||||
insta = { version = "1.8.0", features = ["redactions", "yaml"] }
|
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
|
||||||
paste = "1.0.5"
|
paste = "1.0.14"
|
||||||
pretty_assertions = "1.2.1"
|
pretty_assertions = "1.4.0"
|
||||||
serde_yaml = "0.8.21"
|
serde_yaml = "0.8.26"
|
||||||
tempfile = "3.2.0"
|
tempfile = "3.10.1"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.5.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div> </div>
|
<div> </div>
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a>
|
<a href="https://github.com/rojo-rbx/rojo/actions"><img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" /></a>
|
||||||
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a>
|
<a href="https://crates.io/crates/rojo"><img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" /></a>
|
||||||
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a>
|
<a href="https://rojo.space/docs"><img src="https://img.shields.io/badge/docs-website-brightgreen.svg" alt="Rojo Documentation" /></a>
|
||||||
<a href="https://www.patreon.com/lpghatguy"><img src="https://img.shields.io/badge/sponsor-patreon-red" alt="Patreon" /></a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
@@ -41,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.58.1 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
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.
|
||||||
|
|
||||||
## 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 +1,5 @@
|
|||||||
[tools]
|
[tools]
|
||||||
wally = "UpliftGames/wally@0.3.1"
|
rojo = "rojo-rbx/rojo@7.3.0"
|
||||||
rojo = "rojo-rbx/rojo@7.2.1"
|
selene = "Kampfkarren/selene@0.26.1"
|
||||||
selene = "Kampfkarren/selene@0.20.0"
|
stylua = "JohnnyMorganz/stylua@0.18.2"
|
||||||
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -2,7 +2,7 @@
|
|||||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
To build this library or plugin, use:
|
To build this library, use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rojo build -o "{project_name}.rbxmx"
|
rojo build -o "{project_name}.rbxmx"
|
||||||
|
|||||||
17
assets/default-plugin-project/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# {project_name}
|
||||||
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
To build this plugin to your local plugins folder, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo build -p "{project_name}.rbxm"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can include the `watch` flag to re-build it on save:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo build -p "{project_name}.rbxm" --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, check out [the Rojo documentation](https://rojo.space/docs).
|
||||||
6
assets/default-plugin-project/default.project.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "{project_name}",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
assets/default-plugin-project/gitignore.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Plugin model files
|
||||||
|
/{project_name}.rbxmx
|
||||||
|
/{project_name}.rbxm
|
||||||
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
BIN
assets/images/icons/debug.png
Normal file
|
After Width: | Height: | Size: 183 B |
BIN
assets/images/icons/expand.png
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
assets/images/icons/reset.png
Normal file
|
After Width: | Height: | Size: 933 B |
BIN
assets/images/icons/warning.png
Normal file
|
After Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
BIN
assets/images/syncsuccess.png
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
assets/images/syncwarning.png
Normal file
|
After Width: | Height: | Size: 607 B |
@@ -31,11 +31,12 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
|||||||
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let input = input_path.as_ref().to_path_buf();
|
let input = input_path.as_ref().to_path_buf();
|
||||||
let output = dir.path().join("output.rbxlx");
|
let output = Some(dir.path().join("output.rbxlx"));
|
||||||
|
|
||||||
let options = BuildCommand {
|
let options = BuildCommand {
|
||||||
project: input,
|
project: input,
|
||||||
watch: false,
|
watch: false,
|
||||||
|
plugin: None,
|
||||||
output,
|
output,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
13
build.rs
@@ -7,6 +7,7 @@ use fs_err as fs;
|
|||||||
use fs_err::File;
|
use fs_err::File;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use memofs::VfsSnapshot;
|
use memofs::VfsSnapshot;
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
@@ -43,6 +44,15 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||||
|
|
||||||
|
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||||
|
let plugin_version =
|
||||||
|
Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
our_version, plugin_version,
|
||||||
|
"plugin version does not match Cargo version"
|
||||||
|
);
|
||||||
|
|
||||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||||
@@ -51,10 +61,11 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||||
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
||||||
|
"Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
|
||||||
});
|
});
|
||||||
|
|
||||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
let out_file = File::create(&out_path)?;
|
let out_file = File::create(out_path)?;
|
||||||
|
|
||||||
bincode::serialize_into(out_file, &snapshot)?;
|
bincode::serialize_into(out_file, &snapshot)?;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## 0.3.0 (2024-03-15)
|
||||||
|
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
||||||
|
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
||||||
|
|
||||||
|
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||||
|
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||||
|
|
||||||
## 0.2.0 (2021-08-23)
|
## 0.2.0 (2021-08-23)
|
||||||
* Updated to `crossbeam-channel` 0.5.1.
|
* Updated to `crossbeam-channel` 0.5.1.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -11,7 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossbeam-channel = "0.5.1"
|
crossbeam-channel = "0.5.12"
|
||||||
fs-err = "2.3.0"
|
fs-err = "2.11.0"
|
||||||
notify = "4.0.15"
|
notify = "4.0.17"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ impl InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for InMemoryFs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InMemoryFsInner {
|
struct InMemoryFsInner {
|
||||||
entries: HashMap<PathBuf, Entry>,
|
entries: HashMap<PathBuf, Entry>,
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ mod noop_backend;
|
|||||||
mod snapshot;
|
mod snapshot;
|
||||||
mod std_backend;
|
mod std_backend;
|
||||||
|
|
||||||
use std::io;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
use std::{io, str};
|
||||||
|
|
||||||
pub use in_memory_fs::InMemoryFs;
|
pub use in_memory_fs::InMemoryFs;
|
||||||
pub use noop_backend::NoopBackend;
|
pub use noop_backend::NoopBackend;
|
||||||
@@ -155,6 +155,24 @@ impl VfsInner {
|
|||||||
Ok(Arc::new(contents))
|
Ok(Arc::new(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let contents = self.backend.read(path)?;
|
||||||
|
|
||||||
|
if self.watch_enabled {
|
||||||
|
self.backend.watch(path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents_str = str::from_utf8(&contents).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("File was not valid UTF-8: {}", path.display()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Arc::new(contents_str.into()))
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -194,11 +212,8 @@ impl VfsInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||||
match event {
|
if let VfsEvent::Remove(path) = event {
|
||||||
VfsEvent::Remove(path) => {
|
let _ = self.backend.unwatch(path);
|
||||||
let _ = self.backend.unwatch(&path);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -261,6 +276,33 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().read(path)
|
self.inner.lock().unwrap().read(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a file from the VFS (or from the underlying backend if it isn't
|
||||||
|
/// resident) into a string.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
|
||||||
|
///
|
||||||
|
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
||||||
|
#[inline]
|
||||||
|
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.lock().unwrap().read_to_string(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a file from the VFS (or the underlying backend if it isn't
|
||||||
|
/// resident) into a string, and normalize its line endings to LF.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
|
||||||
|
/// line ending normalization.
|
||||||
|
///
|
||||||
|
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
||||||
|
#[inline]
|
||||||
|
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let contents = self.inner.lock().unwrap().read_to_string(path)?;
|
||||||
|
|
||||||
|
Ok(contents.replace("\r\n", "\n").into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a file to the VFS and the underlying backend.
|
/// Write a file to the VFS and the underlying backend.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||||
@@ -431,3 +473,23 @@ impl VfsLock<'_> {
|
|||||||
self.inner.commit_event(event)
|
self.inner.commit_event(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{InMemoryFs, Vfs, VfsSnapshot};
|
||||||
|
|
||||||
|
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||||
|
#[test]
|
||||||
|
fn read_to_string_lf_normalized_keeps_trailing_newline() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
|
||||||
|
"bar\nfoo\n\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,3 +74,9 @@ impl VfsBackend for NoopBackend {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for NoopBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::io;
|
use std::path::{Path, PathBuf};
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::{collections::HashSet, io};
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
@@ -13,6 +13,7 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
|||||||
pub struct StdBackend {
|
pub struct StdBackend {
|
||||||
watcher: RecommendedWatcher,
|
watcher: RecommendedWatcher,
|
||||||
watcher_receiver: Receiver<VfsEvent>,
|
watcher_receiver: Receiver<VfsEvent>,
|
||||||
|
watches: HashSet<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StdBackend {
|
impl StdBackend {
|
||||||
@@ -48,6 +49,7 @@ impl StdBackend {
|
|||||||
Self {
|
Self {
|
||||||
watcher,
|
watcher,
|
||||||
watcher_receiver: rx,
|
watcher_receiver: rx,
|
||||||
|
watches: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,14 +99,30 @@ impl VfsBackend for StdBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
if self.watches.contains(path)
|
||||||
|
|| path
|
||||||
|
.ancestors()
|
||||||
|
.any(|ancestor| self.watches.contains(ancestor))
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.watches.insert(path.to_path_buf());
|
||||||
self.watcher
|
self.watcher
|
||||||
.watch(path, RecursiveMode::NonRecursive)
|
.watch(path, RecursiveMode::Recursive)
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
self.watches.remove(path);
|
||||||
self.watcher
|
self.watcher
|
||||||
.unwatch(path)
|
.unwatch(path)
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for StdBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ edition = "2018"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0.99"
|
serde = "1.0.197"
|
||||||
serde_yaml = "0.8.9"
|
serde_yaml = "0.8.26"
|
||||||
|
|||||||
1
plugin/Packages/Flipper
Submodule
1
plugin/Packages/Highlighter
Submodule
1
plugin/Packages/Promise
Submodule
1
plugin/Packages/Roact
Submodule
1
plugin/Packages/TestEZ
Submodule
1
plugin/Packages/t
Submodule
1
plugin/Version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7.4.0
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
},
|
},
|
||||||
"Packages": {
|
"Packages": {
|
||||||
"$path": "Packages",
|
"$path": "Packages",
|
||||||
|
|
||||||
"Log": {
|
"Log": {
|
||||||
"$path": "log"
|
"$path": "log"
|
||||||
},
|
},
|
||||||
@@ -20,6 +19,9 @@
|
|||||||
"RbxDom": {
|
"RbxDom": {
|
||||||
"$path": "rbx_dom_lua"
|
"$path": "rbx_dom_lua"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Version": {
|
||||||
|
"$path": "Version.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,12 @@ Error.__index = Error
|
|||||||
|
|
||||||
Error.Kind = {
|
Error.Kind = {
|
||||||
HttpNotEnabled = {
|
HttpNotEnabled = {
|
||||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
message = "Rojo requires HTTP access, which is not enabled.\n"
|
||||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
.. "Check your game settings, located in the 'Home' tab of Studio.",
|
||||||
},
|
},
|
||||||
ConnectFailed = {
|
ConnectFailed = {
|
||||||
message = "Couldn't connect to the Rojo server.\n" ..
|
message = "Couldn't connect to the Rojo server.\n"
|
||||||
"Make sure the server is running — use 'rojo serve' to run it!",
|
.. "Make sure the server is running — use 'rojo serve' to run it!",
|
||||||
},
|
},
|
||||||
Timeout = {
|
Timeout = {
|
||||||
message = "HTTP request timed out.",
|
message = "HTTP request timed out.",
|
||||||
@@ -63,4 +63,13 @@ function Error.fromRobloxErrorString(message)
|
|||||||
return Error.new(Error.Kind.Unknown, message)
|
return Error.new(Error.Kind.Unknown, message)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Error.fromResponse(response)
|
||||||
|
local lower = (response.body or ""):lower()
|
||||||
|
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
|
||||||
|
return Error.new(Error.Kind.Timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
|
||||||
|
end
|
||||||
|
|
||||||
return Error
|
return Error
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ local function performRequest(requestParams)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
|
Log.trace("Request {} success, response {:#?}", requestId, response)
|
||||||
resolve(HttpResponse.fromRobloxResponse(response))
|
local httpResponse = HttpResponse.fromRobloxResponse(response)
|
||||||
|
if httpResponse:isSuccess() then
|
||||||
|
resolve(httpResponse)
|
||||||
|
else
|
||||||
|
reject(HttpError.fromResponse(httpResponse))
|
||||||
|
end
|
||||||
else
|
else
|
||||||
Log.trace("Request {} failure: {:?}", requestId, response)
|
Log.trace("Request {} failure: {:?}", requestId, response)
|
||||||
reject(HttpError.fromRobloxErrorString(response))
|
reject(HttpError.fromRobloxErrorString(response))
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ types = {
|
|||||||
if ok then
|
if ok then
|
||||||
output[key] = result
|
output[key] = result
|
||||||
else
|
else
|
||||||
local warning = ("Could not decode attribute value of type %q: %s"):format(typeof(value), tostring(result))
|
local warning = ("Could not decode attribute value of type %q: %s"):format(
|
||||||
|
typeof(value),
|
||||||
|
tostring(result)
|
||||||
|
)
|
||||||
warn(warning)
|
warn(warning)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -53,7 +56,10 @@ types = {
|
|||||||
if ok then
|
if ok then
|
||||||
output[key] = result
|
output[key] = result
|
||||||
else
|
else
|
||||||
local warning = ("Could not encode attribute value of type %q: %s"):format(typeof(value), tostring(result))
|
local warning = ("Could not encode attribute value of type %q: %s"):format(
|
||||||
|
typeof(value),
|
||||||
|
tostring(result)
|
||||||
|
)
|
||||||
warn(warning)
|
warn(warning)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -111,6 +117,7 @@ types = {
|
|||||||
local pos = pod.position
|
local pos = pod.position
|
||||||
local orient = pod.orientation
|
local orient = pod.orientation
|
||||||
|
|
||||||
|
--stylua: ignore
|
||||||
return CFrame.new(
|
return CFrame.new(
|
||||||
pos[1], pos[2], pos[3],
|
pos[1], pos[2], pos[3],
|
||||||
orient[1][1], orient[1][2], orient[1][3],
|
orient[1][1], orient[1][2], orient[1][3],
|
||||||
@@ -120,10 +127,7 @@ types = {
|
|||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
local x, y, z,
|
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents()
|
||||||
r00, r01, r02,
|
|
||||||
r10, r11, r12,
|
|
||||||
r20, r21, r22 = roblox:GetComponents()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position = { x, y, z },
|
position = { x, y, z },
|
||||||
@@ -161,10 +165,7 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
for index, keypoint in ipairs(pod.keypoints) do
|
||||||
keypoints[index] = ColorSequenceKeypoint.new(
|
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color))
|
||||||
keypoint.time,
|
|
||||||
types.Color3.fromPod(keypoint.color)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return ColorSequence.new(keypoints)
|
return ColorSequence.new(keypoints)
|
||||||
@@ -265,6 +266,27 @@ types = {
|
|||||||
toPod = identity,
|
toPod = identity,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
MaterialColors = {
|
||||||
|
fromPod = function(pod: { [string]: { number } })
|
||||||
|
local real = {}
|
||||||
|
for name, color in pod do
|
||||||
|
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
|
||||||
|
end
|
||||||
|
return real
|
||||||
|
end,
|
||||||
|
toPod = function(roblox: { [Enum.Material]: Color3 })
|
||||||
|
local pod = {}
|
||||||
|
for material, color in roblox do
|
||||||
|
pod[material.Name] = {
|
||||||
|
math.round(math.clamp(color.R, 0, 1) * 255),
|
||||||
|
math.round(math.clamp(color.G, 0, 1) * 255),
|
||||||
|
math.round(math.clamp(color.B, 0, 1) * 255),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return pod
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
NumberRange = {
|
NumberRange = {
|
||||||
fromPod = unpackDecoder(NumberRange.new),
|
fromPod = unpackDecoder(NumberRange.new),
|
||||||
|
|
||||||
@@ -278,11 +300,7 @@ 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(
|
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
|
||||||
keypoint.time,
|
|
||||||
keypoint.value,
|
|
||||||
keypoint.envelope
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return NumberSequence.new(keypoints)
|
return NumberSequence.new(keypoints)
|
||||||
@@ -337,10 +355,7 @@ types = {
|
|||||||
|
|
||||||
Ray = {
|
Ray = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Ray.new(
|
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction))
|
||||||
types.Vector3.fromPod(pod.origin),
|
|
||||||
types.Vector3.fromPod(pod.direction)
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -353,10 +368,7 @@ types = {
|
|||||||
|
|
||||||
Rect = {
|
Rect = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Rect.new(
|
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2]))
|
||||||
types.Vector2.fromPod(pod[1]),
|
|
||||||
types.Vector2.fromPod(pod[2])
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -368,31 +380,28 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Ref = {
|
Ref = {
|
||||||
fromPod = function(_pod)
|
fromPod = function(_)
|
||||||
error("Ref cannot be decoded on its own")
|
error("Ref cannot be decoded on its own")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(_roblox)
|
toPod = function(_)
|
||||||
error("Ref can not be encoded on its own")
|
error("Ref can not be encoded on its own")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
Region3 = {
|
Region3 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(_)
|
||||||
error("Region3 is not implemented")
|
error("Region3 is not implemented")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(_)
|
||||||
error("Region3 is not implemented")
|
error("Region3 is not implemented")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
Region3int16 = {
|
Region3int16 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Region3int16.new(
|
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2]))
|
||||||
types.Vector3int16.fromPod(pod[1]),
|
|
||||||
types.Vector3int16.fromPod(pod[2])
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -404,11 +413,11 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
SharedString = {
|
SharedString = {
|
||||||
fromPod = function(pod)
|
fromPod = function(_pod)
|
||||||
error("SharedString is not supported")
|
error("SharedString is not supported")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(_roblox)
|
||||||
error("SharedString is not supported")
|
error("SharedString is not supported")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -428,10 +437,7 @@ types = {
|
|||||||
|
|
||||||
UDim2 = {
|
UDim2 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return UDim2.new(
|
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2]))
|
||||||
types.UDim.fromPod(pod[1]),
|
|
||||||
types.UDim.fromPod(pod[2])
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -487,9 +493,32 @@ types = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
types.OptionalCFrame = {
|
||||||
|
fromPod = function(pod)
|
||||||
|
if pod == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return types.CFrame.fromPod(pod)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
toPod = function(roblox)
|
||||||
|
if roblox == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return types.CFrame.toPod(roblox)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
function EncodedValue.decode(encodedValue)
|
function EncodedValue.decode(encodedValue)
|
||||||
local ty, value = next(encodedValue)
|
local ty, value = next(encodedValue)
|
||||||
|
|
||||||
|
if ty == nil then
|
||||||
|
-- If the encoded pair is empty, assume it is an unoccupied optional value
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
local typeImpl = types[ty]
|
local typeImpl = types[ty]
|
||||||
if typeImpl == nil then
|
if typeImpl == nil then
|
||||||
return false, "Couldn't decode value " .. tostring(ty)
|
return false, "Couldn't decode value " .. tostring(ty)
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
return function()
|
|
||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local EncodedValue = require(script.Parent.EncodedValue)
|
|
||||||
local allValues = require(script.Parent.allValues)
|
|
||||||
|
|
||||||
local function deepEq(a, b)
|
|
||||||
if typeof(a) ~= typeof(b) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local ty = typeof(a)
|
|
||||||
|
|
||||||
if ty == "table" then
|
|
||||||
local visited = {}
|
|
||||||
|
|
||||||
for key, valueA in pairs(a) do
|
|
||||||
visited[key] = true
|
|
||||||
|
|
||||||
if not deepEq(valueA, b[key]) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, valueB in pairs(b) do
|
|
||||||
if visited[key] then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
if not deepEq(valueB, a[key]) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return a == b
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local extraAssertions = {
|
|
||||||
CFrame = function(value)
|
|
||||||
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
for testName, testEntry in pairs(allValues) do
|
|
||||||
it("round trip " .. testName, function()
|
|
||||||
local ok, decoded = EncodedValue.decode(testEntry.value)
|
|
||||||
assert(ok, decoded)
|
|
||||||
|
|
||||||
if extraAssertions[testName] ~= nil then
|
|
||||||
extraAssertions[testName](decoded)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
|
|
||||||
assert(ok, encoded)
|
|
||||||
|
|
||||||
if not deepEq(encoded, testEntry.value) then
|
|
||||||
local expected = HttpService:JSONEncode(testEntry.value)
|
|
||||||
local actual = HttpService:JSONEncode(encoded)
|
|
||||||
|
|
||||||
local message = string.format(
|
|
||||||
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
|
|
||||||
expected, actual
|
|
||||||
)
|
|
||||||
|
|
||||||
error(message)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -230,6 +230,118 @@
|
|||||||
},
|
},
|
||||||
"ty": "Int64"
|
"ty": "Int64"
|
||||||
},
|
},
|
||||||
|
"MaterialColors": {
|
||||||
|
"value": {
|
||||||
|
"MaterialColors": {
|
||||||
|
"Grass": [
|
||||||
|
106,
|
||||||
|
127,
|
||||||
|
63
|
||||||
|
],
|
||||||
|
"Slate": [
|
||||||
|
63,
|
||||||
|
127,
|
||||||
|
107
|
||||||
|
],
|
||||||
|
"Concrete": [
|
||||||
|
127,
|
||||||
|
102,
|
||||||
|
63
|
||||||
|
],
|
||||||
|
"Brick": [
|
||||||
|
138,
|
||||||
|
86,
|
||||||
|
62
|
||||||
|
],
|
||||||
|
"Sand": [
|
||||||
|
143,
|
||||||
|
126,
|
||||||
|
95
|
||||||
|
],
|
||||||
|
"WoodPlanks": [
|
||||||
|
139,
|
||||||
|
109,
|
||||||
|
79
|
||||||
|
],
|
||||||
|
"Rock": [
|
||||||
|
102,
|
||||||
|
108,
|
||||||
|
111
|
||||||
|
],
|
||||||
|
"Glacier": [
|
||||||
|
101,
|
||||||
|
176,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
"Snow": [
|
||||||
|
195,
|
||||||
|
199,
|
||||||
|
218
|
||||||
|
],
|
||||||
|
"Sandstone": [
|
||||||
|
137,
|
||||||
|
90,
|
||||||
|
71
|
||||||
|
],
|
||||||
|
"Mud": [
|
||||||
|
58,
|
||||||
|
46,
|
||||||
|
36
|
||||||
|
],
|
||||||
|
"Basalt": [
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
37
|
||||||
|
],
|
||||||
|
"Ground": [
|
||||||
|
102,
|
||||||
|
92,
|
||||||
|
59
|
||||||
|
],
|
||||||
|
"CrackedLava": [
|
||||||
|
232,
|
||||||
|
156,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"Asphalt": [
|
||||||
|
115,
|
||||||
|
123,
|
||||||
|
107
|
||||||
|
],
|
||||||
|
"Cobblestone": [
|
||||||
|
132,
|
||||||
|
123,
|
||||||
|
90
|
||||||
|
],
|
||||||
|
"Ice": [
|
||||||
|
129,
|
||||||
|
194,
|
||||||
|
224
|
||||||
|
],
|
||||||
|
"LeafyGrass": [
|
||||||
|
115,
|
||||||
|
132,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"Salt": [
|
||||||
|
198,
|
||||||
|
189,
|
||||||
|
181
|
||||||
|
],
|
||||||
|
"Limestone": [
|
||||||
|
206,
|
||||||
|
173,
|
||||||
|
148
|
||||||
|
],
|
||||||
|
"Pavement": [
|
||||||
|
148,
|
||||||
|
148,
|
||||||
|
140
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "MaterialColors"
|
||||||
|
},
|
||||||
"NumberRange": {
|
"NumberRange": {
|
||||||
"value": {
|
"value": {
|
||||||
"NumberRange": [
|
"NumberRange": [
|
||||||
@@ -258,6 +370,41 @@
|
|||||||
},
|
},
|
||||||
"ty": "NumberSequence"
|
"ty": "NumberSequence"
|
||||||
},
|
},
|
||||||
|
"OptionalCFrame-None": {
|
||||||
|
"value": {
|
||||||
|
"OptionalCFrame": null
|
||||||
|
},
|
||||||
|
"ty": "OptionalCFrame"
|
||||||
|
},
|
||||||
|
"OptionalCFrame-Some": {
|
||||||
|
"value": {
|
||||||
|
"OptionalCFrame": {
|
||||||
|
"position": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"orientation": [
|
||||||
|
[
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "OptionalCFrame"
|
||||||
|
},
|
||||||
"PhysicalProperties-Custom": {
|
"PhysicalProperties-Custom": {
|
||||||
"value": {
|
"value": {
|
||||||
"PhysicalProperties": {
|
"PhysicalProperties": {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
return function()
|
|
||||||
local base64 = require(script.Parent.base64)
|
|
||||||
|
|
||||||
it("should encode and decode", function()
|
|
||||||
local function try(str, expected)
|
|
||||||
local encoded = base64.encode(str)
|
|
||||||
expect(encoded).to.equal(expected)
|
|
||||||
expect(base64.decode(encoded)).to.equal(str)
|
|
||||||
end
|
|
||||||
|
|
||||||
try("Man", "TWFu")
|
|
||||||
try("Ma", "TWE=")
|
|
||||||
try("M", "TQ==")
|
|
||||||
try("ManM", "TWFuTQ==")
|
|
||||||
try(
|
|
||||||
[[Man is distinguished, not only by his reason, but by this ]]..
|
|
||||||
[[singular passion from other animals, which is a lust of the ]]..
|
|
||||||
[[mind, that by a perseverance of delight in the continued and ]]..
|
|
||||||
[[indefatigable generation of knowledge, exceeds the short ]]..
|
|
||||||
[[vehemence of any carnal pleasure.]],
|
|
||||||
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
|
|
||||||
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
|
|
||||||
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
|
|
||||||
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
|
|
||||||
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
|
|
||||||
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
@@ -1,4 +1,45 @@
|
|||||||
local CollectionService = game:GetService("CollectionService")
|
local CollectionService = game:GetService("CollectionService")
|
||||||
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||||
|
|
||||||
|
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||||
|
local TERRAIN_MATERIAL_COLORS = {
|
||||||
|
Enum.Material.Grass,
|
||||||
|
Enum.Material.Slate,
|
||||||
|
Enum.Material.Concrete,
|
||||||
|
Enum.Material.Brick,
|
||||||
|
Enum.Material.Sand,
|
||||||
|
Enum.Material.WoodPlanks,
|
||||||
|
Enum.Material.Rock,
|
||||||
|
Enum.Material.Glacier,
|
||||||
|
Enum.Material.Snow,
|
||||||
|
Enum.Material.Sandstone,
|
||||||
|
Enum.Material.Mud,
|
||||||
|
Enum.Material.Basalt,
|
||||||
|
Enum.Material.Ground,
|
||||||
|
Enum.Material.CrackedLava,
|
||||||
|
Enum.Material.Asphalt,
|
||||||
|
Enum.Material.Cobblestone,
|
||||||
|
Enum.Material.Ice,
|
||||||
|
Enum.Material.LeafyGrass,
|
||||||
|
Enum.Material.Salt,
|
||||||
|
Enum.Material.Limestone,
|
||||||
|
Enum.Material.Pavement,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isAttributeNameValid(attributeName)
|
||||||
|
-- For SetAttribute to succeed, the attribute name must be less than or
|
||||||
|
-- equal to 100 characters...
|
||||||
|
return #attributeName <= 100
|
||||||
|
-- ...and must only contain alphanumeric characters, periods, hyphens,
|
||||||
|
-- underscores, or forward slashes.
|
||||||
|
and attributeName:match("[^%w%.%-_/]") == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isAttributeNameReserved(attributeName)
|
||||||
|
-- For SetAttribute to succeed, attribute names must not use the RBX
|
||||||
|
-- prefix, which is reserved by Roblox.
|
||||||
|
return attributeName:sub(1, 3) == "RBX"
|
||||||
|
end
|
||||||
|
|
||||||
-- Defines how to read and write properties that aren't directly scriptable.
|
-- Defines how to read and write properties that aren't directly scriptable.
|
||||||
--
|
--
|
||||||
@@ -11,18 +52,40 @@ return {
|
|||||||
end,
|
end,
|
||||||
write = function(instance, _, value)
|
write = function(instance, _, value)
|
||||||
local existing = instance:GetAttributes()
|
local existing = instance:GetAttributes()
|
||||||
|
local didAllWritesSucceed = true
|
||||||
|
|
||||||
for key, attr in pairs(value) do
|
for attributeName, attributeValue in pairs(value) do
|
||||||
instance:SetAttribute(key, attr)
|
if isAttributeNameReserved(attributeName) then
|
||||||
|
-- If the attribute name is reserved, then we don't
|
||||||
|
-- really care about reporting any failures about
|
||||||
|
-- it.
|
||||||
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
for key in pairs(existing) do
|
if not isAttributeNameValid(attributeName) then
|
||||||
if value[key] == nil then
|
didAllWritesSucceed = false
|
||||||
instance:SetAttribute(key, nil)
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
instance:SetAttribute(attributeName, attributeValue)
|
||||||
|
end
|
||||||
|
|
||||||
|
for existingAttributeName in pairs(existing) do
|
||||||
|
if isAttributeNameReserved(existingAttributeName) then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isAttributeNameValid(existingAttributeName) then
|
||||||
|
didAllWritesSucceed = false
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if value[existingAttributeName] == nil then
|
||||||
|
instance:SetAttribute(existingAttributeName, nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return didAllWritesSucceed
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
Tags = {
|
Tags = {
|
||||||
@@ -52,10 +115,10 @@ return {
|
|||||||
},
|
},
|
||||||
LocalizationTable = {
|
LocalizationTable = {
|
||||||
Contents = {
|
Contents = {
|
||||||
read = function(instance, key)
|
read = function(instance, _)
|
||||||
return true, instance:GetContents()
|
return true, instance:GetContents()
|
||||||
end,
|
end,
|
||||||
write = function(instance, key, value)
|
write = function(instance, _, value)
|
||||||
instance:SetContents(value)
|
instance:SetContents(value)
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
@@ -70,5 +133,68 @@ return {
|
|||||||
return true, instance:ScaleTo(value)
|
return true, instance:ScaleTo(value)
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
WorldPivotData = {
|
||||||
|
read = function(instance)
|
||||||
|
return true, instance.WorldPivot
|
||||||
|
end,
|
||||||
|
write = function(instance, _, value)
|
||||||
|
if value == nil then
|
||||||
|
return true, nil
|
||||||
|
else
|
||||||
|
instance.WorldPivot = value
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Terrain = {
|
||||||
|
MaterialColors = {
|
||||||
|
read = function(instance: Terrain)
|
||||||
|
-- There's no way to get a list of every color, so we have to
|
||||||
|
-- make one.
|
||||||
|
local colors = {}
|
||||||
|
for _, material in TERRAIN_MATERIAL_COLORS do
|
||||||
|
colors[material] = instance:GetMaterialColor(material)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, colors
|
||||||
|
end,
|
||||||
|
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||||
|
for material, color in value do
|
||||||
|
instance:SetMaterialColor(material, color)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Script = {
|
||||||
|
Source = {
|
||||||
|
read = function(instance: Script)
|
||||||
|
return true, ScriptEditorService:GetEditorSource(instance)
|
||||||
|
end,
|
||||||
|
write = function(instance: Script, _, value: string)
|
||||||
|
task.spawn(function()
|
||||||
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||||
|
return value
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ModuleScript = {
|
||||||
|
Source = {
|
||||||
|
read = function(instance: ModuleScript)
|
||||||
|
return true, ScriptEditorService:GetEditorSource(instance)
|
||||||
|
end,
|
||||||
|
write = function(instance: ModuleScript, _, value: string)
|
||||||
|
task.spawn(function()
|
||||||
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||||
|
return value
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
|
|||||||
return PropertyDescriptor.fromRaw(
|
return PropertyDescriptor.fromRaw(
|
||||||
currentClass.Properties[aliasData.AliasFor],
|
currentClass.Properties[aliasData.AliasFor],
|
||||||
currentClassName,
|
currentClassName,
|
||||||
aliasData.AliasFor)
|
aliasData.AliasFor
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
return function()
|
|
||||||
local RbxDom = require(script.Parent)
|
|
||||||
|
|
||||||
it("should load", function()
|
|
||||||
expect(RbxDom).to.be.ok()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
@@ -11,13 +11,6 @@ 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 validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns a promise that will never resolve nor reject.
|
|
||||||
]]
|
|
||||||
local function hangingPromise()
|
|
||||||
return Promise.new(function() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function rejectFailedRequests(response)
|
local function rejectFailedRequests(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
||||||
@@ -31,15 +24,17 @@ end
|
|||||||
local function rejectWrongProtocolVersion(infoResponseBody)
|
local function rejectWrongProtocolVersion(infoResponseBody)
|
||||||
if infoResponseBody.protocolVersion ~= Config.protocolVersion then
|
if infoResponseBody.protocolVersion ~= Config.protocolVersion then
|
||||||
local message = (
|
local message = (
|
||||||
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible."
|
||||||
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
.. "\nMake sure you have matching versions of both the Rojo plugin and server!"
|
||||||
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
.. "\n\nYour client is version %s, with protocol version %s. It expects server version %s."
|
||||||
"\nYour server is version %s, with protocol version %s." ..
|
.. "\nYour server is version %s, with protocol version %s."
|
||||||
"\n\nGo to https://github.com/rojo-rbx/rojo for more details."
|
.. "\n\nGo to https://github.com/rojo-rbx/rojo for more details."
|
||||||
):format(
|
):format(
|
||||||
Version.display(Config.version), Config.protocolVersion,
|
Version.display(Config.version),
|
||||||
|
Config.protocolVersion,
|
||||||
Config.expectedServerVersionString,
|
Config.expectedServerVersionString,
|
||||||
infoResponseBody.serverVersion, infoResponseBody.protocolVersion
|
infoResponseBody.serverVersion,
|
||||||
|
infoResponseBody.protocolVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
return Promise.reject(message)
|
return Promise.reject(message)
|
||||||
@@ -66,14 +61,11 @@ local function rejectWrongPlaceId(infoResponseBody)
|
|||||||
end
|
end
|
||||||
|
|
||||||
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 %s, 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(
|
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
|
||||||
tostring(game.PlaceId),
|
|
||||||
table.concat(idList, "\n")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.reject(message)
|
return Promise.reject(message)
|
||||||
end
|
end
|
||||||
@@ -93,6 +85,7 @@ function ApiContext.new(baseUrl)
|
|||||||
__sessionId = nil,
|
__sessionId = nil,
|
||||||
__messageCursor = -1,
|
__messageCursor = -1,
|
||||||
__connected = true,
|
__connected = true,
|
||||||
|
__activeRequests = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, ApiContext)
|
return setmetatable(self, ApiContext)
|
||||||
@@ -113,6 +106,11 @@ end
|
|||||||
|
|
||||||
function ApiContext:disconnect()
|
function ApiContext:disconnect()
|
||||||
self.__connected = false
|
self.__connected = false
|
||||||
|
for request in self.__activeRequests do
|
||||||
|
Log.trace("Cancelling request {}", request)
|
||||||
|
request:cancel()
|
||||||
|
end
|
||||||
|
self.__activeRequests = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:setMessageCursor(index)
|
function ApiContext:setMessageCursor(index)
|
||||||
@@ -142,10 +140,7 @@ end
|
|||||||
function ApiContext:read(ids)
|
function ApiContext:read(ids)
|
||||||
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
|
|
||||||
return Http.get(url)
|
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
@@ -190,13 +185,10 @@ function ApiContext:write(patch)
|
|||||||
|
|
||||||
body = Http.jsonEncode(body)
|
body = Http.jsonEncode(body)
|
||||||
|
|
||||||
return Http.post(url, body)
|
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody)
|
||||||
:andThen(rejectFailedRequests)
|
Log.info("Write response: {:?}", responseBody)
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
Log.info("Write response: {:?}", body)
|
|
||||||
|
|
||||||
return body
|
return responseBody
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -204,24 +196,25 @@ function ApiContext:retrieveMessages()
|
|||||||
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||||
|
|
||||||
local function sendRequest()
|
local function sendRequest()
|
||||||
return Http.get(url)
|
local request = Http.get(url):catch(function(err)
|
||||||
:catch(function(err)
|
if err.type == Http.Error.Kind.Timeout and self.__connected then
|
||||||
if err.type == Http.Error.Kind.Timeout then
|
|
||||||
if self.__connected then
|
|
||||||
return sendRequest()
|
return sendRequest()
|
||||||
else
|
|
||||||
return hangingPromise()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Log.trace("Tracking request {}", request)
|
||||||
|
self.__activeRequests[request] = true
|
||||||
|
|
||||||
|
return request:finally(function(...)
|
||||||
|
Log.trace("Cleaning up request {}", request)
|
||||||
|
self.__activeRequests[request] = nil
|
||||||
|
return ...
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return sendRequest()
|
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
@@ -237,10 +230,7 @@ end
|
|||||||
function ApiContext:open(id)
|
function ApiContext:open(id)
|
||||||
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
||||||
|
|
||||||
return Http.post(url, "")
|
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ end
|
|||||||
|
|
||||||
function Checkbox:didUpdate(lastProps)
|
function Checkbox:didUpdate(lastProps)
|
||||||
if lastProps.active ~= self.props.active then
|
if lastProps.active ~= self.props.active then
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, {
|
||||||
Flipper.Spring.new(self.props.active and 1 or 0, {
|
|
||||||
frequency = 6,
|
frequency = 6,
|
||||||
dampingRatio = 1.1,
|
dampingRatio = 1.1,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,10 +49,16 @@ function Checkbox:render()
|
|||||||
ZIndex = self.props.zIndex,
|
ZIndex = self.props.zIndex,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Event.Activated] = self.props.onClick,
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.props.onClick()
|
||||||
|
end,
|
||||||
}, {
|
}, {
|
||||||
StateTip = e(Tooltip.Trigger, {
|
StateTip = e(Tooltip.Trigger, {
|
||||||
text = if self.props.active then "Enabled" else "Disabled",
|
text = (if self.props.locked then "[LOCKED] " else "")
|
||||||
|
.. (if self.props.active then "Enabled" else "Disabled"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Active = e(SlicedImage, {
|
Active = e(SlicedImage, {
|
||||||
@@ -65,7 +69,7 @@ function Checkbox:render()
|
|||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = Assets.Images.Checkbox.Active,
|
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
||||||
ImageColor3 = theme.Active.IconColor,
|
ImageColor3 = theme.Active.IconColor,
|
||||||
ImageTransparency = activeTransparency,
|
ImageTransparency = activeTransparency,
|
||||||
|
|
||||||
@@ -84,7 +88,9 @@ function Checkbox:render()
|
|||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = Assets.Images.Checkbox.Inactive,
|
Image = if self.props.locked
|
||||||
|
then Assets.Images.Checkbox.Locked
|
||||||
|
else Assets.Images.Checkbox.Inactive,
|
||||||
ImageColor3 = theme.Inactive.IconColor,
|
ImageColor3 = theme.Inactive.IconColor,
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
|
|||||||
126
plugin/src/App/Components/ClassIcon.lua
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
local StudioService = game:GetService("StudioService")
|
||||||
|
local AssetService = game:GetService("AssetService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local EditableImage = require(Plugin.App.Components.EditableImage)
|
||||||
|
|
||||||
|
local imageCache = {}
|
||||||
|
local function getImageSizeAndPixels(image)
|
||||||
|
if not imageCache[image] then
|
||||||
|
local editableImage = AssetService:CreateEditableImageAsync(image)
|
||||||
|
imageCache[image] = {
|
||||||
|
Size = editableImage.Size,
|
||||||
|
Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return imageCache[image].Size, table.clone(imageCache[image].Pixels)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getRecoloredClassIcon(className, color)
|
||||||
|
local iconProps = StudioService:GetClassIcon(className)
|
||||||
|
|
||||||
|
if iconProps and color then
|
||||||
|
local success, editableImageSize, editableImagePixels = pcall(function()
|
||||||
|
local size, pixels = getImageSizeAndPixels(iconProps.Image)
|
||||||
|
|
||||||
|
local minVal, maxVal = math.huge, -math.huge
|
||||||
|
for i = 1, #pixels, 4 do
|
||||||
|
if pixels[i + 3] == 0 then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
||||||
|
|
||||||
|
minVal = math.min(minVal, pixelVal)
|
||||||
|
maxVal = math.max(maxVal, pixelVal)
|
||||||
|
end
|
||||||
|
|
||||||
|
local hue, sat, val = color:ToHSV()
|
||||||
|
for i = 1, #pixels, 4 do
|
||||||
|
if pixels[i + 3] == 0 then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
||||||
|
local newVal = val
|
||||||
|
if minVal < maxVal then
|
||||||
|
-- Remap minVal - maxVal to val*0.9 - val
|
||||||
|
newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
|
||||||
|
end
|
||||||
|
|
||||||
|
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
|
||||||
|
pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
|
||||||
|
end
|
||||||
|
return size, pixels
|
||||||
|
end)
|
||||||
|
if success then
|
||||||
|
iconProps.EditableImagePixels = editableImagePixels
|
||||||
|
iconProps.EditableImageSize = editableImageSize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return iconProps
|
||||||
|
end
|
||||||
|
|
||||||
|
local ClassIcon = Roact.PureComponent:extend("ClassIcon")
|
||||||
|
|
||||||
|
function ClassIcon:init()
|
||||||
|
self.state = {
|
||||||
|
iconProps = nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:updateIcon()
|
||||||
|
local props = self.props
|
||||||
|
local iconProps = getRecoloredClassIcon(props.className, props.color)
|
||||||
|
self:setState({
|
||||||
|
iconProps = iconProps,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:didMount()
|
||||||
|
self:updateIcon()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:didUpdate(lastProps)
|
||||||
|
if lastProps.className ~= self.props.className or lastProps.color ~= self.props.color then
|
||||||
|
self:updateIcon()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:render()
|
||||||
|
local iconProps = self.state.iconProps
|
||||||
|
if not iconProps then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return e(
|
||||||
|
"ImageLabel",
|
||||||
|
{
|
||||||
|
Size = self.props.size,
|
||||||
|
Position = self.props.position,
|
||||||
|
LayoutOrder = self.props.layoutOrder,
|
||||||
|
AnchorPoint = self.props.anchorPoint,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
Image = iconProps.Image,
|
||||||
|
ImageRectOffset = iconProps.ImageRectOffset,
|
||||||
|
ImageRectSize = iconProps.ImageRectSize,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
if iconProps.EditableImagePixels
|
||||||
|
then e(EditableImage, {
|
||||||
|
size = iconProps.EditableImageSize,
|
||||||
|
pixels = iconProps.EditableImagePixels,
|
||||||
|
})
|
||||||
|
else nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ClassIcon
|
||||||
61
plugin/src/App/Components/CodeLabel.lua
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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
|
||||||
@@ -29,13 +29,17 @@ function Dropdown:init()
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Dropdown:didUpdate()
|
function Dropdown:didUpdate(prevProps)
|
||||||
self.openMotor:setGoal(
|
if self.props.locked and not prevProps.locked then
|
||||||
Flipper.Spring.new(self.state.open and 1 or 0, {
|
self:setState({
|
||||||
|
open = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
self.openMotor:setGoal(Flipper.Spring.new(self.state.open and 1 or 0, {
|
||||||
frequency = 6,
|
frequency = 6,
|
||||||
dampingRatio = 1.1,
|
dampingRatio = 1.1,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Dropdown:render()
|
function Dropdown:render()
|
||||||
@@ -46,10 +50,7 @@ function Dropdown:render()
|
|||||||
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(
|
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
|
||||||
text, 15, Enum.Font.GothamMedium,
|
|
||||||
Vector2.new(math.huge, 20)
|
|
||||||
)
|
|
||||||
if textSize.X > width then
|
if textSize.X > width then
|
||||||
width = textSize.X
|
width = textSize.X
|
||||||
end
|
end
|
||||||
@@ -68,6 +69,9 @@ function Dropdown:render()
|
|||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then
|
||||||
|
return
|
||||||
|
end
|
||||||
self:setState({
|
self:setState({
|
||||||
open = false,
|
open = false,
|
||||||
})
|
})
|
||||||
@@ -89,6 +93,9 @@ function Dropdown:render()
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then
|
||||||
|
return
|
||||||
|
end
|
||||||
self:setState({
|
self:setState({
|
||||||
open = not self.state.open,
|
open = not self.state.open,
|
||||||
})
|
})
|
||||||
@@ -101,10 +108,8 @@ function Dropdown:render()
|
|||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
DropArrow = e("ImageLabel", {
|
DropArrow = e("ImageLabel", {
|
||||||
Image = Assets.Images.Dropdown.Arrow,
|
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
||||||
ImageColor3 = self.openBinding:map(function(a)
|
ImageColor3 = theme.IconColor,
|
||||||
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
|
|
||||||
end),
|
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
Size = UDim2.new(0, 18, 0, 18),
|
Size = UDim2.new(0, 18, 0, 18),
|
||||||
@@ -128,7 +133,8 @@ function Dropdown:render()
|
|||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
Options = if self.state.open then e(SlicedImage, {
|
Options = if self.state.open
|
||||||
|
then e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.BackgroundColor,
|
color = theme.BackgroundColor,
|
||||||
position = UDim2.new(1, 0, 1, 3),
|
position = UDim2.new(1, 0, 1, 3),
|
||||||
@@ -159,9 +165,10 @@ function Dropdown:render()
|
|||||||
self.setContentSize(object.AbsoluteContentSize)
|
self.setContentSize(object.AbsoluteContentSize)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
Roact.createFragment(optionButtons),
|
Options = Roact.createFragment(optionButtons),
|
||||||
}),
|
}),
|
||||||
}) else nil,
|
})
|
||||||
|
else nil,
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
41
plugin/src/App/Components/EditableImage.lua
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local EditableImage = Roact.PureComponent:extend("EditableImage")
|
||||||
|
|
||||||
|
function EditableImage:init()
|
||||||
|
self.ref = Roact.createRef()
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:writePixels()
|
||||||
|
local image = self.ref.current
|
||||||
|
if not image then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not self.props.pixels then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:render()
|
||||||
|
return e("EditableImage", {
|
||||||
|
Size = self.props.size,
|
||||||
|
[Roact.Ref] = self.ref,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:didMount()
|
||||||
|
self:writePixels()
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:didUpdate()
|
||||||
|
self:writePixels()
|
||||||
|
end
|
||||||
|
|
||||||
|
return EditableImage
|
||||||
@@ -38,15 +38,11 @@ function IconButton:render()
|
|||||||
[Roact.Event.Activated] = self.props.onClick,
|
[Roact.Event.Activated] = self.props.onClick,
|
||||||
|
|
||||||
[Roact.Event.MouseEnter] = function()
|
[Roact.Event.MouseEnter] = function()
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(1, HOVER_SPRING_PROPS))
|
||||||
Flipper.Spring.new(1, HOVER_SPRING_PROPS)
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
[Roact.Event.MouseLeave] = function()
|
[Roact.Event.MouseLeave] = function()
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(0, HOVER_SPRING_PROPS))
|
||||||
Flipper.Spring.new(0, HOVER_SPRING_PROPS)
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
|
|||||||
@@ -4,12 +4,133 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local DisplayValue = require(script.Parent.DisplayValue)
|
local DisplayValue = require(script.Parent.DisplayValue)
|
||||||
|
|
||||||
|
local EMPTY_TABLE = {}
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local function ViewDiffButton(props)
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e("TextButton", {
|
||||||
|
Text = "",
|
||||||
|
Size = UDim2.new(0.7, 0, 1, -4),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
[Roact.Event.Activated] = props.onClick,
|
||||||
|
}, {
|
||||||
|
e(BorderedContainer, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
transparency = props.transparency:map(function(t)
|
||||||
|
return 0.5 + (0.5 * t)
|
||||||
|
end),
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Label = e("TextLabel", {
|
||||||
|
Text = "View Diff",
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0, 65, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Icon = e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Icons.Expand,
|
||||||
|
ImageColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
|
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function RowContent(props)
|
||||||
|
local values = props.values
|
||||||
|
local metadata = props.metadata
|
||||||
|
|
||||||
|
if props.showStringDiff and values[1] == "Source" then
|
||||||
|
-- Special case for .Source updates
|
||||||
|
return e(ViewDiffButton, {
|
||||||
|
transparency = props.transparency,
|
||||||
|
onClick = function()
|
||||||
|
if not props.showStringDiff then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
props.showStringDiff(tostring(values[2]), tostring(values[3]))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.showTableDiff and (type(values[2]) == "table" or type(values[3]) == "table") then
|
||||||
|
-- Special case for table properties (like Attributes/Tags)
|
||||||
|
return e(ViewDiffButton, {
|
||||||
|
transparency = props.transparency,
|
||||||
|
onClick = function()
|
||||||
|
if not props.showTableDiff then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
props.showTableDiff(values[2], values[3])
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return Roact.createFragment({
|
||||||
|
ColumnB = e(
|
||||||
|
"Frame",
|
||||||
|
{
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
},
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = values[2],
|
||||||
|
transparency = props.transparency,
|
||||||
|
textColor = if metadata.isWarning
|
||||||
|
then theme.Diff.Warning
|
||||||
|
else theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
ColumnC = e(
|
||||||
|
"Frame",
|
||||||
|
{
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
|
LayoutOrder = 3,
|
||||||
|
},
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = values[3],
|
||||||
|
transparency = props.transparency,
|
||||||
|
textColor = if metadata.isWarning
|
||||||
|
then theme.Diff.Warning
|
||||||
|
else theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
local ChangeList = Roact.Component:extend("ChangeList")
|
local ChangeList = Roact.Component:extend("ChangeList")
|
||||||
|
|
||||||
function ChangeList:init()
|
function ChangeList:init()
|
||||||
@@ -26,16 +147,15 @@ function ChangeList:render()
|
|||||||
return 0.93 + (0.07 * t)
|
return 0.93 + (0.07 * t)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local columnVisibility = props.columnVisibility
|
|
||||||
|
|
||||||
local rows = {}
|
local rows = {}
|
||||||
local pad = {
|
local pad = {
|
||||||
PaddingLeft = UDim.new(0, 5),
|
PaddingLeft = UDim.new(0, 5),
|
||||||
PaddingRight = UDim.new(0, 5),
|
PaddingRight = UDim.new(0, 5),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local headerRow = changes[1]
|
||||||
local headers = e("Frame", {
|
local headers = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 30),
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
BackgroundTransparency = rowTransparency,
|
BackgroundTransparency = rowTransparency,
|
||||||
BackgroundColor3 = theme.Diff.Row,
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
LayoutOrder = 0,
|
LayoutOrder = 0,
|
||||||
@@ -47,39 +167,36 @@ function ChangeList:render()
|
|||||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
}),
|
}),
|
||||||
A = e("TextLabel", {
|
ColumnA = e("TextLabel", {
|
||||||
Visible = columnVisibility[1],
|
Text = tostring(headerRow[1]),
|
||||||
Text = tostring(changes[1][1]),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
Size = UDim2.new(0.3, 0, 1, 0),
|
Size = UDim2.new(0.3, 0, 1, 0),
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
B = e("TextLabel", {
|
ColumnB = e("TextLabel", {
|
||||||
Visible = columnVisibility[2],
|
Text = tostring(headerRow[2]),
|
||||||
Text = tostring(changes[1][2]),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
Size = UDim2.new(0.35, 0, 1, 0),
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 2,
|
||||||
}),
|
}),
|
||||||
C = e("TextLabel", {
|
ColumnC = e("TextLabel", {
|
||||||
Visible = columnVisibility[3],
|
Text = tostring(headerRow[3]),
|
||||||
Text = tostring(changes[1][3]),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
@@ -93,8 +210,11 @@ function ChangeList:render()
|
|||||||
continue -- Skip headers, already handled above
|
continue -- Skip headers, already handled above
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local metadata = values[4] or EMPTY_TABLE
|
||||||
|
local isWarning = metadata.isWarning
|
||||||
|
|
||||||
rows[row] = e("Frame", {
|
rows[row] = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 30),
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||||
BackgroundColor3 = theme.Diff.Row,
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
@@ -107,45 +227,25 @@ function ChangeList:render()
|
|||||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
}),
|
}),
|
||||||
A = e("TextLabel", {
|
ColumnA = e("TextLabel", {
|
||||||
Visible = columnVisibility[1],
|
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||||
Text = tostring(values[1]),
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
Size = UDim2.new(0.3, 0, 1, 0),
|
Size = UDim2.new(0.3, 0, 1, 0),
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
}),
|
}),
|
||||||
B = e(
|
Content = e(RowContent, {
|
||||||
"Frame",
|
values = values,
|
||||||
{
|
metadata = metadata,
|
||||||
Visible = columnVisibility[2],
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
Size = UDim2.new(0.35, 0, 1, 0),
|
|
||||||
LayoutOrder = 2,
|
|
||||||
},
|
|
||||||
e(DisplayValue, {
|
|
||||||
value = values[2],
|
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
})
|
showStringDiff = props.showStringDiff,
|
||||||
),
|
showTableDiff = props.showTableDiff,
|
||||||
C = e(
|
}),
|
||||||
"Frame",
|
|
||||||
{
|
|
||||||
Visible = columnVisibility[3],
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
Size = UDim2.new(0.35, 0, 1, 0),
|
|
||||||
LayoutOrder = 3,
|
|
||||||
},
|
|
||||||
e(DisplayValue, {
|
|
||||||
value = values[3],
|
|
||||||
transparency = props.transparency,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -169,8 +269,8 @@ function ChangeList:render()
|
|||||||
}, {
|
}, {
|
||||||
Headers = headers,
|
Headers = headers,
|
||||||
Values = e(ScrollingFrame, {
|
Values = e(ScrollingFrame, {
|
||||||
size = UDim2.new(1, 0, 1, -30),
|
size = UDim2.new(1, 0, 1, -24),
|
||||||
position = UDim2.new(0, 0, 0, 30),
|
position = UDim2.new(0, 0, 0, 24),
|
||||||
contentSize = self.contentSize,
|
contentSize = self.contentSize,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
}, rows),
|
}, rows),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ local function DisplayValue(props)
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
@@ -42,7 +42,6 @@ local function DisplayValue(props)
|
|||||||
Position = UDim2.new(0, 25, 0, 0),
|
Position = UDim2.new(0, 25, 0, 0),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
elseif t == "table" then
|
elseif t == "table" then
|
||||||
-- Showing a memory address for tables is useless, so we want to show the best we can
|
-- Showing a memory address for tables is useless, so we want to show the best we can
|
||||||
local textRepresentation = nil
|
local textRepresentation = nil
|
||||||
@@ -54,18 +53,33 @@ local function DisplayValue(props)
|
|||||||
elseif next(props.value) == nil then
|
elseif next(props.value) == nil then
|
||||||
-- If it's empty, show empty braces
|
-- If it's empty, show empty braces
|
||||||
textRepresentation = "{}"
|
textRepresentation = "{}"
|
||||||
|
elseif next(props.value) == 1 then
|
||||||
|
-- We don't need to support mixed tables, so checking the first key is enough
|
||||||
|
-- to determine if it's a simple array
|
||||||
|
local out, i = table.create(#props.value), 0
|
||||||
|
for _, v in props.value do
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
-- Wrap strings in quotes
|
||||||
|
if type(v) == "string" then
|
||||||
|
v = '"' .. v .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
out[i] = tostring(v)
|
||||||
|
end
|
||||||
|
textRepresentation = "{ " .. table.concat(out, ", ") .. " }"
|
||||||
else
|
else
|
||||||
-- If it has children, list them out
|
-- Otherwise, show the table contents as a dictionary
|
||||||
local out, i = {}, 0
|
local out, i = {}, 0
|
||||||
for k, v in pairs(props.value) do
|
for k, v in pairs(props.value) do
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
-- Wrap strings in quotes
|
-- Wrap strings in quotes
|
||||||
if type(k) == "string" then
|
if type(k) == "string" then
|
||||||
k = "\"" .. k .. "\""
|
k = '"' .. k .. '"'
|
||||||
end
|
end
|
||||||
if type(v) == "string" then
|
if type(v) == "string" then
|
||||||
v = "\"" .. v .. "\""
|
v = '"' .. v .. '"'
|
||||||
end
|
end
|
||||||
|
|
||||||
out[i] = string.format("[%s] = %s", tostring(k), tostring(v))
|
out[i] = string.format("[%s] = %s", tostring(k), tostring(v))
|
||||||
@@ -78,7 +92,7 @@ local function DisplayValue(props)
|
|||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
@@ -90,12 +104,17 @@ local function DisplayValue(props)
|
|||||||
-- Or special text handling tostring for some?
|
-- Or special text handling tostring for some?
|
||||||
-- Will add as needed, let's see what cases arise.
|
-- Will add as needed, let's see what cases arise.
|
||||||
|
|
||||||
|
local textRepresentation = string.gsub(tostring(props.value), "%s", " ")
|
||||||
|
if t == "string" then
|
||||||
|
textRepresentation = '"' .. textRepresentation .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
return e("TextLabel", {
|
return e("TextLabel", {
|
||||||
Text = string.gsub(tostring(props.value), "%s", " "),
|
Text = textRepresentation,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = props.textColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
local StudioService = game:GetService("StudioService")
|
local SelectionService = game:GetService("Selection")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
@@ -14,6 +14,8 @@ local bindingUtil = require(Plugin.App.bindingUtil)
|
|||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local ChangeList = require(script.Parent.ChangeList)
|
local ChangeList = require(script.Parent.ChangeList)
|
||||||
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
|
local ClassIcon = require(Plugin.App.Components.ClassIcon)
|
||||||
|
|
||||||
local Expansion = Roact.Component:extend("Expansion")
|
local Expansion = Roact.Component:extend("Expansion")
|
||||||
|
|
||||||
@@ -26,13 +28,14 @@ function Expansion:render()
|
|||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(1, -props.indent, 1, -30),
|
Size = UDim2.new(1, -props.indent, 1, -24),
|
||||||
Position = UDim2.new(0, props.indent, 0, 30),
|
Position = UDim2.new(0, props.indent, 0, 24),
|
||||||
}, {
|
}, {
|
||||||
ChangeList = e(ChangeList, {
|
ChangeList = e(ChangeList, {
|
||||||
changes = props.changeList,
|
changes = props.changeList,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
columnVisibility = props.columnVisibility,
|
showStringDiff = props.showStringDiff,
|
||||||
|
showTableDiff = props.showTableDiff,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -40,13 +43,8 @@ end
|
|||||||
local DomLabel = Roact.Component:extend("DomLabel")
|
local DomLabel = Roact.Component:extend("DomLabel")
|
||||||
|
|
||||||
function DomLabel:init()
|
function DomLabel:init()
|
||||||
self.maxElementHeight = 0
|
|
||||||
if self.props.changeList then
|
|
||||||
self.maxElementHeight = math.clamp(#self.props.changeList * 30, 30, 30 * 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
local initHeight = self.props.elementHeight:getValue()
|
local initHeight = self.props.elementHeight:getValue()
|
||||||
self.expanded = initHeight > 30
|
self.expanded = initHeight > 24
|
||||||
|
|
||||||
self.motor = Flipper.SingleMotor.new(initHeight)
|
self.motor = Flipper.SingleMotor.new(initHeight)
|
||||||
self.binding = bindingUtil.fromMotor(self.motor)
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
@@ -55,7 +53,7 @@ function DomLabel:init()
|
|||||||
renderExpansion = self.expanded,
|
renderExpansion = self.expanded,
|
||||||
})
|
})
|
||||||
self.motor:onStep(function(value)
|
self.motor:onStep(function(value)
|
||||||
local renderExpansion = value > 30
|
local renderExpansion = value > 24
|
||||||
|
|
||||||
self.props.setElementHeight(value)
|
self.props.setElementHeight(value)
|
||||||
if self.props.updateEvent then
|
if self.props.updateEvent then
|
||||||
@@ -74,35 +72,77 @@ function DomLabel:init()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function DomLabel:didUpdate(prevProps)
|
||||||
|
if
|
||||||
|
prevProps.instance ~= self.props.instance
|
||||||
|
or prevProps.patchType ~= self.props.patchType
|
||||||
|
or prevProps.name ~= self.props.name
|
||||||
|
or prevProps.changeList ~= self.props.changeList
|
||||||
|
then
|
||||||
|
-- Close the expansion when the domlabel is changed to a different thing
|
||||||
|
self.expanded = false
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(24, {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function DomLabel:render()
|
function DomLabel:render()
|
||||||
local props = self.props
|
local props = self.props
|
||||||
|
local depth = props.depth or 1
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local iconProps = StudioService:GetClassIcon(props.className)
|
local color = if props.isWarning
|
||||||
local indent = (props.depth or 0) * 20 + 25
|
then theme.Diff.Warning
|
||||||
|
elseif props.patchType then theme.Diff[props.patchType]
|
||||||
|
else theme.TextColor
|
||||||
|
|
||||||
|
local indent = (depth - 1) * 12 + 15
|
||||||
|
|
||||||
-- Line guides help indent depth remain readable
|
-- Line guides help indent depth remain readable
|
||||||
local lineGuides = {}
|
local lineGuides = {}
|
||||||
for i = 1, props.depth or 0 do
|
for i = 2, depth do
|
||||||
table.insert(
|
if props.depthsComplete[i] then
|
||||||
lineGuides,
|
continue
|
||||||
e("Frame", {
|
end
|
||||||
Name = "Line_" .. i,
|
if props.isFinalChild and i == depth then
|
||||||
Size = UDim2.new(0, 2, 1, 2),
|
-- This line stops halfway down to merge with our connector for the right angle
|
||||||
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
|
lineGuides["Line_" .. i] = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 2, 0, 15),
|
||||||
|
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = props.transparency,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
-- All other lines go all the way
|
||||||
|
-- with the exception of the final element, which stops halfway down
|
||||||
|
lineGuides["Line_" .. i] = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 2, 1, if props.isFinalElement then -9 else 2),
|
||||||
|
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = props.transparency,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if depth ~= 1 then
|
||||||
|
lineGuides["Connector"] = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 8, 0, 2),
|
||||||
|
Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12),
|
||||||
|
AnchorPoint = Vector2.xAxis,
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
BackgroundTransparency = props.transparency,
|
BackgroundTransparency = props.transparency,
|
||||||
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
})
|
})
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
Name = "Change",
|
|
||||||
ClipsDescendants = true,
|
ClipsDescendants = true,
|
||||||
BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
|
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1,
|
||||||
BorderSizePixel = 0,
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
BackgroundTransparency = props.patchType and props.transparency or 1,
|
|
||||||
Size = self.binding:map(function(expand)
|
Size = self.binding:map(function(expand)
|
||||||
return UDim2.new(1, 0, 0, expand)
|
return UDim2.new(1, 0, 0, expand)
|
||||||
end),
|
end),
|
||||||
@@ -111,66 +151,128 @@ function DomLabel:render()
|
|||||||
PaddingLeft = UDim.new(0, 10),
|
PaddingLeft = UDim.new(0, 10),
|
||||||
PaddingRight = UDim.new(0, 10),
|
PaddingRight = UDim.new(0, 10),
|
||||||
}),
|
}),
|
||||||
ExpandButton = if props.changeList
|
Button = e("TextButton", {
|
||||||
then e("TextButton", {
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Text = "",
|
Text = "",
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
[Roact.Event.Activated] = function()
|
[Roact.Event.Activated] = function(_rbx: Instance, _input: InputObject, clickCount: number)
|
||||||
|
if clickCount == 1 then
|
||||||
|
-- Double click opens the instance in explorer
|
||||||
|
self.lastDoubleClickTime = os.clock()
|
||||||
|
if props.instance then
|
||||||
|
SelectionService:Set({ props.instance })
|
||||||
|
end
|
||||||
|
elseif clickCount == 0 then
|
||||||
|
-- Single click expands the changes
|
||||||
|
task.wait(0.25)
|
||||||
|
if os.clock() - (self.lastDoubleClickTime or 0) <= 0.25 then
|
||||||
|
-- This is a double click, so don't expand
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.changeList then
|
||||||
self.expanded = not self.expanded
|
self.expanded = not self.expanded
|
||||||
self.motor:setGoal(Flipper.Spring.new((self.expanded and self.maxElementHeight or 0) + 30, {
|
local goalHeight = 24
|
||||||
|
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0)
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
|
||||||
frequency = 5,
|
frequency = 5,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
}))
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
|
}, {
|
||||||
|
StateTip = if (props.instance or props.changeList)
|
||||||
|
then e(Tooltip.Trigger, {
|
||||||
|
text = (if props.changeList
|
||||||
|
then "Click to " .. (if self.expanded then "hide" else "view") .. " changes"
|
||||||
|
else "") .. (if props.instance
|
||||||
|
then (if props.changeList then " & d" else "D") .. "ouble click to open in Explorer"
|
||||||
|
else ""),
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
|
}),
|
||||||
Expansion = if props.changeList
|
Expansion = if props.changeList
|
||||||
then e(Expansion, {
|
then e(Expansion, {
|
||||||
rendered = self.state.renderExpansion,
|
rendered = self.state.renderExpansion,
|
||||||
indent = indent,
|
indent = indent,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
changeList = props.changeList,
|
changeList = props.changeList,
|
||||||
columnVisibility = props.columnVisibility,
|
showStringDiff = props.showStringDiff,
|
||||||
|
showTableDiff = props.showTableDiff,
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
DiffIcon = if props.patchType
|
DiffIcon = if props.patchType
|
||||||
then e("ImageLabel", {
|
then e("ImageLabel", {
|
||||||
Image = Assets.Images.Diff[props.patchType],
|
Image = Assets.Images.Diff[props.patchType],
|
||||||
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
ImageColor3 = color,
|
||||||
ImageTransparency = props.transparency,
|
ImageTransparency = props.transparency,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Size = UDim2.new(0, 20, 0, 20),
|
Size = UDim2.new(0, 14, 0, 14),
|
||||||
Position = UDim2.new(0, 0, 0, 15),
|
Position = UDim2.new(0, 0, 0, 12),
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
ClassIcon = e("ImageLabel", {
|
ClassIcon = e(ClassIcon, {
|
||||||
Image = iconProps.Image,
|
className = props.className,
|
||||||
ImageTransparency = props.transparency,
|
color = color,
|
||||||
ImageRectOffset = iconProps.ImageRectOffset,
|
transparency = props.transparency,
|
||||||
ImageRectSize = iconProps.ImageRectSize,
|
size = UDim2.new(0, 16, 0, 16),
|
||||||
BackgroundTransparency = 1,
|
position = UDim2.new(0, indent + 2, 0, 12),
|
||||||
Size = UDim2.new(0, 20, 0, 20),
|
anchorPoint = Vector2.new(0, 0.5),
|
||||||
Position = UDim2.new(0, indent, 0, 15),
|
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
|
||||||
}),
|
}),
|
||||||
InstanceName = e("TextLabel", {
|
InstanceName = e("TextLabel", {
|
||||||
Text = props.name .. (props.hint and string.format(
|
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
||||||
' <font color="#%s">%s</font>',
|
|
||||||
theme.AddressEntry.PlaceholderColor:ToHex(),
|
|
||||||
props.hint
|
|
||||||
) or ""),
|
|
||||||
RichText = true,
|
RichText = true,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
Font = Enum.Font.GothamMedium,
|
Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
TextColor3 = color,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = props.transparency,
|
TextTransparency = props.transparency,
|
||||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
Size = UDim2.new(1, -indent - 50, 0, 30),
|
Size = UDim2.new(1, -indent - 50, 0, 24),
|
||||||
Position = UDim2.new(0, indent + 30, 0, 0),
|
Position = UDim2.new(0, indent + 22, 0, 0),
|
||||||
|
}),
|
||||||
|
ChangeInfo = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, -indent - 80, 0, 24),
|
||||||
|
Position = UDim2.new(1, -2, 0, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
Edits = if props.changeInfo and props.changeInfo.edits
|
||||||
|
then e("TextLabel", {
|
||||||
|
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.SubTextColor,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 0, 16),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
Failed = if props.changeInfo and props.changeInfo.failed
|
||||||
|
then e("TextLabel", {
|
||||||
|
Text = props.changeInfo.failed,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Diff.Warning,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 0, 16),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
LayoutOrder = 6,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
}),
|
}),
|
||||||
LineGuides = e("Folder", nil, lineGuides),
|
LineGuides = e("Folder", nil, lineGuides),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,143 +1,18 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Log = require(Packages.Log)
|
|
||||||
|
|
||||||
|
local PatchTree = require(Plugin.PatchTree)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
|
||||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
|
||||||
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local function alphabeticalNext(t, state)
|
|
||||||
-- Equivalent of the next function, but returns the keys in the alphabetic
|
|
||||||
-- order of node names. We use a temporary ordered key table that is stored in the
|
|
||||||
-- table being iterated.
|
|
||||||
|
|
||||||
local key = nil
|
|
||||||
if state == nil then
|
|
||||||
-- First iteration, generate the index
|
|
||||||
local orderedIndex, i = table.create(5), 0
|
|
||||||
for k in t do
|
|
||||||
i += 1
|
|
||||||
orderedIndex[i] = k
|
|
||||||
end
|
|
||||||
table.sort(orderedIndex, function(a, b)
|
|
||||||
local nodeA, nodeB = t[a], t[b]
|
|
||||||
return (nodeA.name or "") < (nodeB.name or "")
|
|
||||||
end)
|
|
||||||
|
|
||||||
t.__orderedIndex = orderedIndex
|
|
||||||
key = orderedIndex[1]
|
|
||||||
else
|
|
||||||
-- Fetch the next value
|
|
||||||
for i, orderedState in t.__orderedIndex do
|
|
||||||
if orderedState == state then
|
|
||||||
key = t.__orderedIndex[i + 1]
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if key then
|
|
||||||
return key, t[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
-- No more value to return, cleanup
|
|
||||||
t.__orderedIndex = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local function alphabeticalPairs(t)
|
|
||||||
-- Equivalent of the pairs() iterator, but sorted
|
|
||||||
return alphabeticalNext, t, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function Tree()
|
|
||||||
local tree = {
|
|
||||||
idToNode = {},
|
|
||||||
ROOT = {
|
|
||||||
className = "DataModel",
|
|
||||||
name = "ROOT",
|
|
||||||
children = {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
-- Add ROOT to idToNode or it won't be found by getNode since that searches *within* ROOT
|
|
||||||
tree.idToNode["ROOT"] = tree.ROOT
|
|
||||||
|
|
||||||
function tree:getNode(id, target)
|
|
||||||
if self.idToNode[id] then
|
|
||||||
return self.idToNode[id]
|
|
||||||
end
|
|
||||||
|
|
||||||
for nodeId, node in target or tree.ROOT.children do
|
|
||||||
if nodeId == id then
|
|
||||||
self.idToNode[id] = node
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
local descendant = self:getNode(id, node.children)
|
|
||||||
if descendant then
|
|
||||||
return descendant
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function tree:addNode(parent, props)
|
|
||||||
parent = parent or "ROOT"
|
|
||||||
|
|
||||||
local node = self:getNode(props.id)
|
|
||||||
if node then
|
|
||||||
for k, v in props do
|
|
||||||
node[k] = v
|
|
||||||
end
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
node = table.clone(props)
|
|
||||||
node.children = {}
|
|
||||||
|
|
||||||
local parentNode = self:getNode(parent)
|
|
||||||
if not parentNode then
|
|
||||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
parentNode.children[node.id] = node
|
|
||||||
self.idToNode[node.id] = node
|
|
||||||
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
function tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
-- Build nodes for ancestry by going up the tree
|
|
||||||
local previousId = "ROOT"
|
|
||||||
for _, ancestorId in ancestry do
|
|
||||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
|
||||||
if not value then
|
|
||||||
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
self:addNode(previousId, {
|
|
||||||
id = ancestorId,
|
|
||||||
className = value.ClassName,
|
|
||||||
name = value.Name,
|
|
||||||
})
|
|
||||||
previousId = ancestorId
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return tree
|
|
||||||
end
|
|
||||||
|
|
||||||
local DomLabel = require(script.DomLabel)
|
local DomLabel = require(script.DomLabel)
|
||||||
|
|
||||||
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
|
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
|
||||||
@@ -153,239 +28,113 @@ function PatchVisualizer:willUnmount()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function PatchVisualizer:shouldUpdate(nextProps)
|
function PatchVisualizer:shouldUpdate(nextProps)
|
||||||
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
if self.props.patchTree ~= nextProps.patchTree then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
||||||
|
if currentPatch ~= nil or nextPatch ~= nil then
|
||||||
return not PatchSet.isEqual(currentPatch, nextPatch)
|
return not PatchSet.isEqual(currentPatch, nextPatch)
|
||||||
end
|
end
|
||||||
|
|
||||||
function PatchVisualizer:buildTree(patch, instanceMap)
|
return false
|
||||||
local tree = Tree()
|
|
||||||
|
|
||||||
for _, change in patch.updated do
|
|
||||||
local instance = instanceMap.fromIds[change.id]
|
|
||||||
if not instance then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gather ancestors from existing DOM
|
|
||||||
local ancestry = {}
|
|
||||||
local parentObject = instance.Parent
|
|
||||||
local parentId = instanceMap.fromInstances[parentObject]
|
|
||||||
while parentObject do
|
|
||||||
table.insert(ancestry, 1, parentId)
|
|
||||||
parentObject = parentObject.Parent
|
|
||||||
parentId = instanceMap.fromInstances[parentObject]
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
|
|
||||||
-- Gather detail text
|
|
||||||
local changeList, hint = nil, nil
|
|
||||||
if next(change.changedProperties) or change.changedName then
|
|
||||||
changeList = {}
|
|
||||||
|
|
||||||
local hintBuffer, i = {}, 0
|
|
||||||
local function addProp(prop: string, current: any?, incoming: any?)
|
|
||||||
i += 1
|
|
||||||
hintBuffer[i] = prop
|
|
||||||
changeList[i] = { prop, current, incoming }
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gather the changes
|
|
||||||
|
|
||||||
if change.changedName then
|
|
||||||
addProp("Name", instance.Name, change.changedName)
|
|
||||||
end
|
|
||||||
|
|
||||||
for prop, incoming in change.changedProperties do
|
|
||||||
local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
|
|
||||||
local currentSuccess, currentValue = getProperty(instance, prop)
|
|
||||||
|
|
||||||
addProp(
|
|
||||||
prop,
|
|
||||||
if currentSuccess then currentValue else "[Error]",
|
|
||||||
if incomingSuccess then incomingValue else next(incoming)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Finalize detail values
|
|
||||||
|
|
||||||
-- Trim hint to top 3
|
|
||||||
table.sort(hintBuffer)
|
|
||||||
if #hintBuffer > 3 then
|
|
||||||
hintBuffer = {
|
|
||||||
hintBuffer[1],
|
|
||||||
hintBuffer[2],
|
|
||||||
hintBuffer[3],
|
|
||||||
i - 3 .. " more",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
hint = table.concat(hintBuffer, ", ")
|
|
||||||
|
|
||||||
-- Sort changes and add header
|
|
||||||
table.sort(changeList, function(a, b)
|
|
||||||
return a[1] < b[1]
|
|
||||||
end)
|
|
||||||
table.insert(changeList, 1, { "Property", "Current", "Incoming" })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Add this node to tree
|
|
||||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
|
||||||
id = change.id,
|
|
||||||
patchType = "Edit",
|
|
||||||
className = instance.ClassName,
|
|
||||||
name = instance.Name,
|
|
||||||
hint = hint,
|
|
||||||
changeList = changeList,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, instance in patch.removed do
|
|
||||||
-- Gather ancestors from existing DOM
|
|
||||||
-- (note that they may have no ID if they're being removed as unknown)
|
|
||||||
local ancestry = {}
|
|
||||||
local parentObject = instance.Parent
|
|
||||||
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
|
||||||
while parentObject do
|
|
||||||
instanceMap:insert(parentId, parentObject)
|
|
||||||
table.insert(ancestry, 1, parentId)
|
|
||||||
parentObject = parentObject.Parent
|
|
||||||
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
|
|
||||||
-- Add this node to tree
|
|
||||||
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
|
||||||
instanceMap:insert(nodeId, instance)
|
|
||||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
|
||||||
id = nodeId,
|
|
||||||
patchType = "Remove",
|
|
||||||
className = instance.ClassName,
|
|
||||||
name = instance.Name,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, change in patch.added do
|
|
||||||
-- Gather ancestors from existing DOM or future additions
|
|
||||||
local ancestry = {}
|
|
||||||
local parentId = change.Parent
|
|
||||||
local parentData = patch.added[parentId]
|
|
||||||
local parentObject = instanceMap.fromIds[parentId]
|
|
||||||
while parentId do
|
|
||||||
table.insert(ancestry, 1, parentId)
|
|
||||||
parentId = nil
|
|
||||||
|
|
||||||
if parentData then
|
|
||||||
parentId = parentData.Parent
|
|
||||||
parentData = patch.added[parentId]
|
|
||||||
parentObject = instanceMap.fromIds[parentId]
|
|
||||||
elseif parentObject then
|
|
||||||
parentObject = parentObject.Parent
|
|
||||||
parentId = instanceMap.fromInstances[parentObject]
|
|
||||||
parentData = patch.added[parentId]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestry, patch, instanceMap)
|
|
||||||
|
|
||||||
-- Gather detail text
|
|
||||||
local changeList, hint = nil, nil
|
|
||||||
if next(change.Properties) then
|
|
||||||
changeList = {}
|
|
||||||
|
|
||||||
local hintBuffer, i = {}, 0
|
|
||||||
for prop, incoming in change.Properties do
|
|
||||||
i += 1
|
|
||||||
hintBuffer[i] = prop
|
|
||||||
|
|
||||||
local success, incomingValue = decodeValue(incoming, instanceMap)
|
|
||||||
if success then
|
|
||||||
table.insert(changeList, { prop, "N/A", incomingValue })
|
|
||||||
else
|
|
||||||
table.insert(changeList, { prop, "N/A", next(incoming) })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Finalize detail values
|
|
||||||
|
|
||||||
-- Trim hint to top 3
|
|
||||||
table.sort(hintBuffer)
|
|
||||||
if #hintBuffer > 3 then
|
|
||||||
hintBuffer = {
|
|
||||||
hintBuffer[1],
|
|
||||||
hintBuffer[2],
|
|
||||||
hintBuffer[3],
|
|
||||||
i - 3 .. " more",
|
|
||||||
}
|
|
||||||
end
|
|
||||||
hint = table.concat(hintBuffer, ", ")
|
|
||||||
|
|
||||||
-- Sort changes and add header
|
|
||||||
table.sort(changeList, function(a, b)
|
|
||||||
return a[1] < b[1]
|
|
||||||
end)
|
|
||||||
table.insert(changeList, 1, { "Property", "Current", "Incoming" })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Add this node to tree
|
|
||||||
tree:addNode(change.Parent, {
|
|
||||||
id = change.Id,
|
|
||||||
patchType = "Add",
|
|
||||||
className = change.ClassName,
|
|
||||||
name = change.Name,
|
|
||||||
hint = hint,
|
|
||||||
changeList = changeList,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return tree
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function PatchVisualizer:render()
|
function PatchVisualizer:render()
|
||||||
local patch = self.props.patch
|
local patchTree = self.props.patchTree
|
||||||
local instanceMap = self.props.instanceMap
|
if patchTree == nil and self.props.patch ~= nil then
|
||||||
|
patchTree = PatchTree.build(
|
||||||
local tree = self:buildTree(patch, instanceMap)
|
self.props.patch,
|
||||||
|
self.props.instanceMap,
|
||||||
|
self.props.changeListHeaders or { "Property", "Current", "Incoming" }
|
||||||
|
)
|
||||||
|
if self.props.unappliedPatch then
|
||||||
|
patchTree =
|
||||||
|
PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Recusively draw tree
|
-- Recusively draw tree
|
||||||
local scrollElements, elementHeights = {}, {}
|
local scrollElements, elementHeights, elementIndex = {}, {}, 0
|
||||||
|
|
||||||
|
if patchTree then
|
||||||
|
local elementTotal = patchTree:getCount()
|
||||||
|
local depthsComplete = {}
|
||||||
local function drawNode(node, depth)
|
local function drawNode(node, depth)
|
||||||
local elementHeight, setElementHeight = Roact.createBinding(30)
|
elementIndex += 1
|
||||||
table.insert(elementHeights, elementHeight)
|
|
||||||
table.insert(
|
local parentNode = patchTree:getNode(node.parentId)
|
||||||
scrollElements,
|
local isFinalChild = true
|
||||||
e(DomLabel, {
|
if parentNode then
|
||||||
columnVisibility = self.props.columnVisibility,
|
for _id, sibling in parentNode.children do
|
||||||
|
if type(sibling) == "table" and sibling.name and sibling.name > node.name then
|
||||||
|
isFinalChild = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local elementHeight, setElementHeight = Roact.createBinding(24)
|
||||||
|
elementHeights[elementIndex] = elementHeight
|
||||||
|
scrollElements[elementIndex] = e(DomLabel, {
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
showStringDiff = self.props.showStringDiff,
|
||||||
|
showTableDiff = self.props.showTableDiff,
|
||||||
updateEvent = self.updateEvent,
|
updateEvent = self.updateEvent,
|
||||||
elementHeight = elementHeight,
|
elementHeight = elementHeight,
|
||||||
setElementHeight = setElementHeight,
|
setElementHeight = setElementHeight,
|
||||||
|
elementIndex = elementIndex,
|
||||||
|
isFinalElement = elementIndex == elementTotal,
|
||||||
|
depth = depth,
|
||||||
|
depthsComplete = table.clone(depthsComplete),
|
||||||
|
hasChildren = (node.children ~= nil and next(node.children) ~= nil),
|
||||||
|
isFinalChild = isFinalChild,
|
||||||
patchType = node.patchType,
|
patchType = node.patchType,
|
||||||
className = node.className,
|
className = node.className,
|
||||||
|
isWarning = node.isWarning,
|
||||||
|
instance = node.instance,
|
||||||
name = node.name,
|
name = node.name,
|
||||||
hint = node.hint,
|
changeInfo = node.changeInfo,
|
||||||
changeList = node.changeList,
|
changeList = node.changeList,
|
||||||
depth = depth,
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
for _, childNode in alphabeticalPairs(node.children) do
|
if isFinalChild then
|
||||||
drawNode(childNode, depth + 1)
|
depthsComplete[depth] = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, node in alphabeticalPairs(tree.ROOT.children) do
|
|
||||||
drawNode(node, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
patchTree:forEach(function(node, depth)
|
||||||
|
depthsComplete[depth] = false
|
||||||
|
for i = depth + 1, #depthsComplete do
|
||||||
|
depthsComplete[i] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
drawNode(node, depth)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
return e(BorderedContainer, {
|
return e(BorderedContainer, {
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
size = self.props.size,
|
size = self.props.size,
|
||||||
position = self.props.position,
|
position = self.props.position,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
layoutOrder = self.props.layoutOrder,
|
layoutOrder = self.props.layoutOrder,
|
||||||
}, {
|
}, {
|
||||||
|
CleanMerge = e("TextLabel", {
|
||||||
|
Visible = #scrollElements == 0,
|
||||||
|
Text = "No changes to sync, project is up to date.",
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextWrapped = true,
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
|
||||||
VirtualScroller = e(VirtualScroller, {
|
VirtualScroller = e(VirtualScroller, {
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, -2),
|
||||||
|
position = UDim2.new(0, 0, 0, 2),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
count = #scrollElements,
|
count = #scrollElements,
|
||||||
updateEvent = self.updateEvent.Event,
|
updateEvent = self.updateEvent.Event,
|
||||||
@@ -397,6 +146,7 @@ function PatchVisualizer:render()
|
|||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return PatchVisualizer
|
return PatchVisualizer
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ local bindingUtil = require(Plugin.App.bindingUtil)
|
|||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local scrollDirToAutoSize = {
|
||||||
|
[Enum.ScrollingDirection.X] = Enum.AutomaticSize.X,
|
||||||
|
[Enum.ScrollingDirection.Y] = Enum.AutomaticSize.Y,
|
||||||
|
[Enum.ScrollingDirection.XY] = Enum.AutomaticSize.XY,
|
||||||
|
}
|
||||||
|
|
||||||
local function ScrollingFrame(props)
|
local function ScrollingFrame(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e("ScrollingFrame", {
|
return e("ScrollingFrame", {
|
||||||
@@ -23,19 +29,31 @@ local function ScrollingFrame(props)
|
|||||||
BottomImage = Assets.Images.ScrollBar.Bottom,
|
BottomImage = Assets.Images.ScrollBar.Bottom,
|
||||||
|
|
||||||
ElasticBehavior = Enum.ElasticBehavior.Always,
|
ElasticBehavior = Enum.ElasticBehavior.Always,
|
||||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
ScrollingDirection = props.scrollingDirection or Enum.ScrollingDirection.Y,
|
||||||
|
|
||||||
Size = props.size,
|
Size = props.size,
|
||||||
Position = props.position,
|
Position = props.position,
|
||||||
AnchorPoint = props.anchorPoint,
|
AnchorPoint = props.anchorPoint,
|
||||||
CanvasSize = props.contentSize:map(function(value)
|
CanvasSize = if props.contentSize
|
||||||
return UDim2.new(0, 0, 0, value.Y)
|
then props.contentSize:map(function(value)
|
||||||
end),
|
return UDim2.new(
|
||||||
|
0,
|
||||||
|
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
|
||||||
|
then value.X
|
||||||
|
else 0,
|
||||||
|
0,
|
||||||
|
value.Y
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
else UDim2.new(),
|
||||||
|
AutomaticCanvasSize = if props.contentSize == nil
|
||||||
|
then scrollDirToAutoSize[props.scrollingDirection or Enum.ScrollingDirection.XY]
|
||||||
|
else nil,
|
||||||
|
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize]
|
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize],
|
||||||
}, props[Roact.Children])
|
}, props[Roact.Children])
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ local function SlicedImage(props)
|
|||||||
Size = props.size,
|
Size = props.size,
|
||||||
Position = props.position,
|
Position = props.position,
|
||||||
AnchorPoint = props.anchorPoint,
|
AnchorPoint = props.anchorPoint,
|
||||||
|
AutomaticSize = props.automaticSize,
|
||||||
|
|
||||||
ZIndex = props.zIndex,
|
ZIndex = props.zIndex,
|
||||||
LayoutOrder = props.layoutOrder,
|
LayoutOrder = props.layoutOrder,
|
||||||
|
|||||||
441
plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
--[[
|
||||||
|
Based on DiffMatchPatch by Neil Fraser.
|
||||||
|
https://github.com/google/diff-match-patch
|
||||||
|
]]
|
||||||
|
|
||||||
|
export type DiffAction = number
|
||||||
|
export type Diff = { actionType: DiffAction, value: string }
|
||||||
|
export type Diffs = { Diff }
|
||||||
|
|
||||||
|
local StringDiff = {
|
||||||
|
ActionTypes = table.freeze({
|
||||||
|
Equal = 0,
|
||||||
|
Delete = 1,
|
||||||
|
Insert = 2,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
function StringDiff.findDiffs(text1: string, text2: string): Diffs
|
||||||
|
-- Validate inputs
|
||||||
|
if type(text1) ~= "string" or type(text2) ~= "string" then
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"Invalid inputs to StringDiff.findDiffs, expected strings and got (%s, %s)",
|
||||||
|
type(text1),
|
||||||
|
type(text2)
|
||||||
|
),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shortcut if the texts are identical
|
||||||
|
if text1 == text2 then
|
||||||
|
return { { actionType = StringDiff.ActionTypes.Equal, value = text1 } }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trim off any shared prefix and suffix
|
||||||
|
-- These are easy to detect and can be dealt with quickly without needing a complex diff
|
||||||
|
-- and later we simply add them as Equal to the start and end of the diff
|
||||||
|
local sharedPrefix, sharedSuffix
|
||||||
|
local prefixLength = StringDiff._sharedPrefix(text1, text2)
|
||||||
|
if prefixLength > 0 then
|
||||||
|
-- Store the prefix
|
||||||
|
sharedPrefix = string.sub(text1, 1, prefixLength)
|
||||||
|
-- Now trim it off
|
||||||
|
text1 = string.sub(text1, prefixLength + 1)
|
||||||
|
text2 = string.sub(text2, prefixLength + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local suffixLength = StringDiff._sharedSuffix(text1, text2)
|
||||||
|
if suffixLength > 0 then
|
||||||
|
-- Store the suffix
|
||||||
|
sharedSuffix = string.sub(text1, -suffixLength)
|
||||||
|
-- Now trim it off
|
||||||
|
text1 = string.sub(text1, 1, -suffixLength - 1)
|
||||||
|
text2 = string.sub(text2, 1, -suffixLength - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compute the diff on the middle block where the changes lie
|
||||||
|
local diffs = StringDiff._computeDiff(text1, text2)
|
||||||
|
|
||||||
|
-- Restore the prefix and suffix
|
||||||
|
if sharedPrefix then
|
||||||
|
table.insert(diffs, 1, { actionType = StringDiff.ActionTypes.Equal, value = sharedPrefix })
|
||||||
|
end
|
||||||
|
if sharedSuffix then
|
||||||
|
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = sharedSuffix })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup the diff
|
||||||
|
diffs = StringDiff._reorderAndMerge(diffs)
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._sharedPrefix(text1: string, text2: string): number
|
||||||
|
-- Uses a binary search to find the largest common prefix between the two strings
|
||||||
|
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
|
||||||
|
|
||||||
|
-- Shortcut common cases
|
||||||
|
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, 1) ~= string.byte(text2, 1)) then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local pointerMin = 1
|
||||||
|
local pointerMax = math.min(#text1, #text2)
|
||||||
|
local pointerMid = pointerMax
|
||||||
|
local pointerStart = 1
|
||||||
|
while pointerMin < pointerMid do
|
||||||
|
if string.sub(text1, pointerStart, pointerMid) == string.sub(text2, pointerStart, pointerMid) then
|
||||||
|
pointerMin = pointerMid
|
||||||
|
pointerStart = pointerMin
|
||||||
|
else
|
||||||
|
pointerMax = pointerMid
|
||||||
|
end
|
||||||
|
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return pointerMid
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._sharedSuffix(text1: string, text2: string): number
|
||||||
|
-- Uses a binary search to find the largest common suffix between the two strings
|
||||||
|
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
|
||||||
|
|
||||||
|
-- Shortcut common cases
|
||||||
|
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, -1) ~= string.byte(text2, -1)) then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local pointerMin = 1
|
||||||
|
local pointerMax = math.min(#text1, #text2)
|
||||||
|
local pointerMid = pointerMax
|
||||||
|
local pointerEnd = 1
|
||||||
|
while pointerMin < pointerMid do
|
||||||
|
if string.sub(text1, -pointerMid, -pointerEnd) == string.sub(text2, -pointerMid, -pointerEnd) then
|
||||||
|
pointerMin = pointerMid
|
||||||
|
pointerEnd = pointerMin
|
||||||
|
else
|
||||||
|
pointerMax = pointerMid
|
||||||
|
end
|
||||||
|
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return pointerMid
|
||||||
|
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._bisect(text1: string, text2: string): Diffs
|
||||||
|
-- Find the 'middle snake' of a diff, split the problem in two
|
||||||
|
-- and return the recursively constructed diff
|
||||||
|
-- See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations
|
||||||
|
|
||||||
|
-- Cache the text lengths to prevent multiple calls
|
||||||
|
local text1Length = #text1
|
||||||
|
local text2Length = #text2
|
||||||
|
|
||||||
|
local _sub, _element
|
||||||
|
local maxD = math.ceil((text1Length + text2Length) / 2)
|
||||||
|
local vOffset = maxD
|
||||||
|
local vLength = 2 * maxD
|
||||||
|
local v1 = table.create(vLength)
|
||||||
|
local v2 = table.create(vLength)
|
||||||
|
|
||||||
|
-- Setting all elements to -1 is faster in Lua than mixing integers and nil
|
||||||
|
for x = 0, vLength - 1 do
|
||||||
|
v1[x] = -1
|
||||||
|
v2[x] = -1
|
||||||
|
end
|
||||||
|
v1[vOffset + 1] = 0
|
||||||
|
v2[vOffset + 1] = 0
|
||||||
|
local delta = text1Length - text2Length
|
||||||
|
|
||||||
|
-- If the total number of characters is odd, then
|
||||||
|
-- the front path will collide with the reverse path
|
||||||
|
local front = (delta % 2 ~= 0)
|
||||||
|
|
||||||
|
-- Offsets for start and end of k loop
|
||||||
|
-- Prevents mapping of space beyond the grid
|
||||||
|
local k1Start = 0
|
||||||
|
local k1End = 0
|
||||||
|
local k2Start = 0
|
||||||
|
local k2End = 0
|
||||||
|
for d = 0, maxD - 1 do
|
||||||
|
-- Walk the front path one step
|
||||||
|
for k1 = -d + k1Start, d - k1End, 2 do
|
||||||
|
local k1_offset = vOffset + k1
|
||||||
|
local x1
|
||||||
|
if (k1 == -d) or ((k1 ~= d) and (v1[k1_offset - 1] < v1[k1_offset + 1])) then
|
||||||
|
x1 = v1[k1_offset + 1]
|
||||||
|
else
|
||||||
|
x1 = v1[k1_offset - 1] + 1
|
||||||
|
end
|
||||||
|
local y1 = x1 - k1
|
||||||
|
while
|
||||||
|
(x1 <= text1Length)
|
||||||
|
and (y1 <= text2Length)
|
||||||
|
and (string.sub(text1, x1, x1) == string.sub(text2, y1, y1))
|
||||||
|
do
|
||||||
|
x1 = x1 + 1
|
||||||
|
y1 = y1 + 1
|
||||||
|
end
|
||||||
|
v1[k1_offset] = x1
|
||||||
|
if x1 > text1Length + 1 then
|
||||||
|
-- Ran off the right of the graph
|
||||||
|
k1End = k1End + 2
|
||||||
|
elseif y1 > text2Length + 1 then
|
||||||
|
-- Ran off the bottom of the graph
|
||||||
|
k1Start = k1Start + 2
|
||||||
|
elseif front then
|
||||||
|
local k2_offset = vOffset + delta - k1
|
||||||
|
if k2_offset >= 0 and k2_offset < vLength and v2[k2_offset] ~= -1 then
|
||||||
|
-- Mirror x2 onto top-left coordinate system
|
||||||
|
local x2 = text1Length - v2[k2_offset] + 1
|
||||||
|
if x1 > x2 then
|
||||||
|
-- Overlap detected
|
||||||
|
return StringDiff._bisectSplit(text1, text2, x1, y1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Walk the reverse path one step
|
||||||
|
for k2 = -d + k2Start, d - k2End, 2 do
|
||||||
|
local k2_offset = vOffset + k2
|
||||||
|
local x2
|
||||||
|
if (k2 == -d) or ((k2 ~= d) and (v2[k2_offset - 1] < v2[k2_offset + 1])) then
|
||||||
|
x2 = v2[k2_offset + 1]
|
||||||
|
else
|
||||||
|
x2 = v2[k2_offset - 1] + 1
|
||||||
|
end
|
||||||
|
local y2 = x2 - k2
|
||||||
|
while
|
||||||
|
(x2 <= text1Length)
|
||||||
|
and (y2 <= text2Length)
|
||||||
|
and (string.sub(text1, -x2, -x2) == string.sub(text2, -y2, -y2))
|
||||||
|
do
|
||||||
|
x2 = x2 + 1
|
||||||
|
y2 = y2 + 1
|
||||||
|
end
|
||||||
|
v2[k2_offset] = x2
|
||||||
|
if x2 > text1Length + 1 then
|
||||||
|
-- Ran off the left of the graph
|
||||||
|
k2End = k2End + 2
|
||||||
|
elseif y2 > text2Length + 1 then
|
||||||
|
-- Ran off the top of the graph
|
||||||
|
k2Start = k2Start + 2
|
||||||
|
elseif not front then
|
||||||
|
local k1_offset = vOffset + delta - k2
|
||||||
|
if k1_offset >= 0 and k1_offset < vLength and v1[k1_offset] ~= -1 then
|
||||||
|
local x1 = v1[k1_offset]
|
||||||
|
local y1 = vOffset + x1 - k1_offset
|
||||||
|
-- Mirror x2 onto top-left coordinate system
|
||||||
|
x2 = text1Length - x2 + 1
|
||||||
|
if x1 > x2 then
|
||||||
|
-- Overlap detected
|
||||||
|
return StringDiff._bisectSplit(text1, text2, x1, y1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Number of diffs equals number of characters, no commonality at all
|
||||||
|
return {
|
||||||
|
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
||||||
|
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._bisectSplit(text1: string, text2: string, x: number, y: number): Diffs
|
||||||
|
-- Given the location of the 'middle snake',
|
||||||
|
-- split the diff in two parts and recurse
|
||||||
|
|
||||||
|
local text1a = string.sub(text1, 1, x - 1)
|
||||||
|
local text2a = string.sub(text2, 1, y - 1)
|
||||||
|
local text1b = string.sub(text1, x)
|
||||||
|
local text2b = string.sub(text2, y)
|
||||||
|
|
||||||
|
-- Compute both diffs serially
|
||||||
|
local diffs = StringDiff.findDiffs(text1a, text2a)
|
||||||
|
local diffsB = StringDiff.findDiffs(text1b, text2b)
|
||||||
|
|
||||||
|
-- Merge diffs
|
||||||
|
table.move(diffsB, 1, #diffsB, #diffs + 1, diffs)
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._reorderAndMerge(diffs: Diffs): Diffs
|
||||||
|
-- Reorder and merge like edit sections and merge equalities
|
||||||
|
-- Any edit section can move as long as it doesn't cross an equality
|
||||||
|
|
||||||
|
-- Add a dummy entry at the end
|
||||||
|
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = "" })
|
||||||
|
|
||||||
|
local pointer = 1
|
||||||
|
local countDelete, countInsert = 0, 0
|
||||||
|
local textDelete, textInsert = "", ""
|
||||||
|
local commonLength
|
||||||
|
while diffs[pointer] do
|
||||||
|
local actionType = diffs[pointer].actionType
|
||||||
|
if actionType == StringDiff.ActionTypes.Insert then
|
||||||
|
countInsert = countInsert + 1
|
||||||
|
textInsert = textInsert .. diffs[pointer].value
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Delete then
|
||||||
|
countDelete = countDelete + 1
|
||||||
|
textDelete = textDelete .. diffs[pointer].value
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Equal then
|
||||||
|
-- Upon reaching an equality, check for prior redundancies
|
||||||
|
if countDelete + countInsert > 1 then
|
||||||
|
if (countDelete > 0) and (countInsert > 0) then
|
||||||
|
-- Factor out any common prefixies
|
||||||
|
commonLength = StringDiff._sharedPrefix(textInsert, textDelete)
|
||||||
|
if commonLength > 0 then
|
||||||
|
local back_pointer = pointer - countDelete - countInsert
|
||||||
|
if
|
||||||
|
(back_pointer > 1) and (diffs[back_pointer - 1].actionType == StringDiff.ActionTypes.Equal)
|
||||||
|
then
|
||||||
|
diffs[back_pointer - 1].value = diffs[back_pointer - 1].value
|
||||||
|
.. string.sub(textInsert, 1, commonLength)
|
||||||
|
else
|
||||||
|
table.insert(diffs, 1, {
|
||||||
|
actionType = StringDiff.ActionTypes.Equal,
|
||||||
|
value = string.sub(textInsert, 1, commonLength),
|
||||||
|
})
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
textInsert = string.sub(textInsert, commonLength + 1)
|
||||||
|
textDelete = string.sub(textDelete, commonLength + 1)
|
||||||
|
end
|
||||||
|
-- Factor out any common suffixies
|
||||||
|
commonLength = StringDiff._sharedSuffix(textInsert, textDelete)
|
||||||
|
if commonLength ~= 0 then
|
||||||
|
diffs[pointer].value = string.sub(textInsert, -commonLength) .. diffs[pointer].value
|
||||||
|
textInsert = string.sub(textInsert, 1, -commonLength - 1)
|
||||||
|
textDelete = string.sub(textDelete, 1, -commonLength - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Delete the offending records and add the merged ones
|
||||||
|
pointer = pointer - countDelete - countInsert
|
||||||
|
for _ = 1, countDelete + countInsert do
|
||||||
|
table.remove(diffs, pointer)
|
||||||
|
end
|
||||||
|
if #textDelete > 0 then
|
||||||
|
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Delete, value = textDelete })
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
if #textInsert > 0 then
|
||||||
|
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Insert, value = textInsert })
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif (pointer > 1) and (diffs[pointer - 1].actionType == StringDiff.ActionTypes.Equal) then
|
||||||
|
-- Merge this equality with the previous one
|
||||||
|
diffs[pointer - 1].value = diffs[pointer - 1].value .. diffs[pointer].value
|
||||||
|
table.remove(diffs, pointer)
|
||||||
|
else
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
countInsert, countDelete = 0, 0
|
||||||
|
textDelete, textInsert = "", ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if diffs[#diffs].value == "" then
|
||||||
|
-- Remove the dummy entry at the end
|
||||||
|
diffs[#diffs] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Second pass: look for single edits surrounded on both sides by equalities
|
||||||
|
-- which can be shifted sideways to eliminate an equality
|
||||||
|
-- e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
|
||||||
|
local changes = false
|
||||||
|
pointer = 2
|
||||||
|
-- Intentionally ignore the first and last element (don't need checking)
|
||||||
|
while pointer < #diffs 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 currentDiff = diffs[pointer]
|
||||||
|
local currentText = currentDiff.value
|
||||||
|
local prevText = prevDiff.value
|
||||||
|
local nextText = nextDiff.value
|
||||||
|
if #prevText == 0 then
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
changes = true
|
||||||
|
elseif string.sub(currentText, -#prevText) == prevText then
|
||||||
|
-- Shift the edit over the previous equality
|
||||||
|
currentDiff.value = prevText .. string.sub(currentText, 1, -#prevText - 1)
|
||||||
|
nextDiff.value = prevText .. nextDiff.value
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
changes = true
|
||||||
|
elseif string.sub(currentText, 1, #nextText) == nextText then
|
||||||
|
-- Shift the edit over the next equality
|
||||||
|
prevDiff.value = prevText .. nextText
|
||||||
|
currentDiff.value = string.sub(currentText, #nextText + 1) .. nextText
|
||||||
|
table.remove(diffs, pointer + 1)
|
||||||
|
changes = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If shifts were made, the diffs need reordering and another shift sweep
|
||||||
|
if changes then
|
||||||
|
return StringDiff._reorderAndMerge(diffs)
|
||||||
|
end
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiff
|
||||||
205
plugin/src/App/Components/StringDiffVisualizer/init.lua
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
local Highlighter = require(Packages.Highlighter)
|
||||||
|
local StringDiff = require(script:FindFirstChild("StringDiff"))
|
||||||
|
|
||||||
|
local Timer = require(Plugin.Timer)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer")
|
||||||
|
|
||||||
|
function StringDiffVisualizer:init()
|
||||||
|
self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0))
|
||||||
|
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
-- Ensure that the script background is up to date with the current theme
|
||||||
|
self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function()
|
||||||
|
task.defer(function()
|
||||||
|
-- Defer to allow Highlighter to process the theme change first
|
||||||
|
self:updateScriptBackground()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
self:calculateContentSize()
|
||||||
|
self:updateScriptBackground()
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
add = {},
|
||||||
|
remove = {},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:willUnmount()
|
||||||
|
self.themeChangedConnection:Disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:updateScriptBackground()
|
||||||
|
local backgroundColor = Highlighter.getTokenColor("background")
|
||||||
|
if backgroundColor ~= self.scriptBackground:getValue() then
|
||||||
|
self.setScriptBackground(backgroundColor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:didUpdate(previousProps)
|
||||||
|
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
||||||
|
self:calculateContentSize()
|
||||||
|
local add, remove = self:calculateDiffLines()
|
||||||
|
self:setState({
|
||||||
|
add = add,
|
||||||
|
remove = remove,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateContentSize()
|
||||||
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
|
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
|
||||||
|
local startClock = os.clock()
|
||||||
|
local diffs = StringDiff.findDiffs(oldString, newString)
|
||||||
|
local stopClock = os.clock()
|
||||||
|
|
||||||
|
Log.trace(
|
||||||
|
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||||
|
#oldString,
|
||||||
|
#newString,
|
||||||
|
math.round((stopClock - startClock) * 1000 * 1000),
|
||||||
|
#diffs
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Determine which lines to highlight
|
||||||
|
local add, remove = {}, {}
|
||||||
|
|
||||||
|
local oldLineNum, newLineNum = 1, 1
|
||||||
|
for _, diff in diffs do
|
||||||
|
local actionType, text = diff.actionType, diff.value
|
||||||
|
local lines = select(2, string.gsub(text, "\n", "\n"))
|
||||||
|
|
||||||
|
if actionType == StringDiff.ActionTypes.Equal then
|
||||||
|
oldLineNum += lines
|
||||||
|
newLineNum += lines
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Insert then
|
||||||
|
if lines > 0 then
|
||||||
|
local textLines = string.split(text, "\n")
|
||||||
|
for i, textLine in textLines do
|
||||||
|
if string.match(textLine, "%S") then
|
||||||
|
add[newLineNum + i - 1] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if string.match(text, "%S") then
|
||||||
|
add[newLineNum] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
newLineNum += lines
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Delete then
|
||||||
|
if lines > 0 then
|
||||||
|
local textLines = string.split(text, "\n")
|
||||||
|
for i, textLine in textLines do
|
||||||
|
if string.match(textLine, "%S") then
|
||||||
|
remove[oldLineNum + i - 1] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if string.match(text, "%S") then
|
||||||
|
remove[oldLineNum] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
oldLineNum += lines
|
||||||
|
else
|
||||||
|
Log.warn("Unknown diff action: {} {}", actionType, text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Timer.stop()
|
||||||
|
return add, remove
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:render()
|
||||||
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e(BorderedContainer, {
|
||||||
|
size = self.props.size,
|
||||||
|
position = self.props.position,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Background = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = self.scriptBackground,
|
||||||
|
ZIndex = -10,
|
||||||
|
}, {
|
||||||
|
UICorner = e("UICorner", {
|
||||||
|
CornerRadius = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Separator = e("Frame", {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
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, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
|
text = oldString,
|
||||||
|
lineBackground = theme.Diff.Remove,
|
||||||
|
markedLines = self.state.remove,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
New = e(ScrollingFrame, {
|
||||||
|
position = UDim2.new(0.5, 5, 0, 2),
|
||||||
|
size = UDim2.new(0.5, -7, 1, -4),
|
||||||
|
scrollingDirection = Enum.ScrollingDirection.XY,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
contentSize = self.contentSize,
|
||||||
|
}, {
|
||||||
|
Source = e(CodeLabel, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
|
text = newString,
|
||||||
|
lineBackground = theme.Diff.Add,
|
||||||
|
markedLines = self.state.add,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiffVisualizer
|
||||||
@@ -14,7 +14,11 @@ local StudioPluginAction = Roact.Component:extend("StudioPluginAction")
|
|||||||
|
|
||||||
function StudioPluginAction:init()
|
function StudioPluginAction:init()
|
||||||
self.pluginAction = self.props.plugin:CreatePluginAction(
|
self.pluginAction = self.props.plugin:CreatePluginAction(
|
||||||
self.props.name, self.props.title, self.props.description, self.props.icon, self.props.bindable
|
self.props.name,
|
||||||
|
self.props.title,
|
||||||
|
self.props.description,
|
||||||
|
self.props.icon,
|
||||||
|
self.props.bindable
|
||||||
)
|
)
|
||||||
|
|
||||||
self.pluginAction.Triggered:Connect(self.props.onTriggered)
|
self.pluginAction.Triggered:Connect(self.props.onTriggered)
|
||||||
@@ -31,9 +35,12 @@ end
|
|||||||
local function StudioPluginActionWrapper(props)
|
local function StudioPluginActionWrapper(props)
|
||||||
return e(StudioPluginContext.Consumer, {
|
return e(StudioPluginContext.Consumer, {
|
||||||
render = function(plugin)
|
render = function(plugin)
|
||||||
return e(StudioPluginAction, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioPluginAction,
|
||||||
|
Dictionary.merge(props, {
|
||||||
plugin = plugin,
|
plugin = plugin,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
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
|
||||||
@@ -36,7 +38,10 @@ function StudioPluginGui:init()
|
|||||||
minimumSize.Y
|
minimumSize.Y
|
||||||
)
|
)
|
||||||
|
|
||||||
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(self.props.id, dockWidgetPluginGuiInfo)
|
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(
|
||||||
|
if self.props.isEphemeral then HttpService:GenerateGUID(false) else self.props.id,
|
||||||
|
dockWidgetPluginGuiInfo
|
||||||
|
)
|
||||||
|
|
||||||
pluginGui.Name = self.props.id
|
pluginGui.Name = self.props.id
|
||||||
pluginGui.Title = self.props.title
|
pluginGui.Title = self.props.title
|
||||||
@@ -76,6 +81,12 @@ function StudioPluginGui:didUpdate(lastProps)
|
|||||||
if self.props.active ~= lastProps.active then
|
if self.props.active ~= lastProps.active then
|
||||||
-- This is intentionally in didUpdate to make sure the initial active state
|
-- This is intentionally in didUpdate to make sure the initial active state
|
||||||
-- (if the PluginGui is open initially) is preserved.
|
-- (if the PluginGui is open initially) is preserved.
|
||||||
|
|
||||||
|
-- Studio widgets are very unreliable and sometimes need to be flickered
|
||||||
|
-- in order to force them to render correctly
|
||||||
|
-- This happens within a single frame so it doesn't flicker visibly
|
||||||
|
self.pluginGui.Enabled = self.props.active
|
||||||
|
self.pluginGui.Enabled = not self.props.active
|
||||||
self.pluginGui.Enabled = self.props.active
|
self.pluginGui.Enabled = self.props.active
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,12 +18,8 @@ StudioToggleButton.defaultProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StudioToggleButton:init()
|
function StudioToggleButton:init()
|
||||||
local button = self.props.toolbar:CreateButton(
|
local button =
|
||||||
self.props.name,
|
self.props.toolbar:CreateButton(self.props.name, self.props.tooltip, self.props.icon, self.props.text)
|
||||||
self.props.tooltip,
|
|
||||||
self.props.icon,
|
|
||||||
self.props.text
|
|
||||||
)
|
|
||||||
|
|
||||||
button.Click:Connect(function()
|
button.Click:Connect(function()
|
||||||
if self.props.onClick then
|
if self.props.onClick then
|
||||||
@@ -61,9 +57,12 @@ end
|
|||||||
local function StudioToggleButtonWrapper(props)
|
local function StudioToggleButtonWrapper(props)
|
||||||
return e(StudioToolbarContext.Consumer, {
|
return e(StudioToolbarContext.Consumer, {
|
||||||
render = function(toolbar)
|
render = function(toolbar)
|
||||||
return e(StudioToggleButton, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioToggleButton,
|
||||||
|
Dictionary.merge(props, {
|
||||||
toolbar = toolbar,
|
toolbar = toolbar,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,9 +36,12 @@ end
|
|||||||
local function StudioToolbarWrapper(props)
|
local function StudioToolbarWrapper(props)
|
||||||
return e(StudioPluginContext.Consumer, {
|
return e(StudioPluginContext.Consumer, {
|
||||||
render = function(plugin)
|
render = function(plugin)
|
||||||
return e(StudioToolbar, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioToolbar,
|
||||||
|
Dictionary.merge(props, {
|
||||||
plugin = plugin,
|
plugin = plugin,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
195
plugin/src/App/Components/TableDiffVisualizer/Array.lua
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Timer = require(Plugin.Timer)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Array = Roact.Component:extend("Array")
|
||||||
|
|
||||||
|
function Array:init()
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Array:calculateDiff()
|
||||||
|
Timer.start("Array:calculateDiff")
|
||||||
|
--[[
|
||||||
|
Find the indexes that are added or removed from the array,
|
||||||
|
and display them side by side with gaps for the indexes that
|
||||||
|
dont exist in the opposite array.
|
||||||
|
]]
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
|
||||||
|
local i, j = 1, 1
|
||||||
|
local diff = {}
|
||||||
|
|
||||||
|
while i <= #oldTable and j <= #newTable do
|
||||||
|
if oldTable[i] == newTable[j] then
|
||||||
|
table.insert(diff, { oldTable[i], newTable[j] }) -- Unchanged
|
||||||
|
i += 1
|
||||||
|
j += 1
|
||||||
|
elseif not table.find(newTable, oldTable[i], j) then
|
||||||
|
table.insert(diff, { oldTable[i], nil }) -- Removal
|
||||||
|
i += 1
|
||||||
|
elseif not table.find(oldTable, newTable[j], i) then
|
||||||
|
table.insert(diff, { nil, newTable[j] }) -- Addition
|
||||||
|
j += 1
|
||||||
|
else
|
||||||
|
if table.find(newTable, oldTable[i], j) then
|
||||||
|
table.insert(diff, { nil, newTable[j] }) -- Addition
|
||||||
|
j += 1
|
||||||
|
else
|
||||||
|
table.insert(diff, { oldTable[i], nil }) -- Removal
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle remaining elements
|
||||||
|
while i <= #oldTable do
|
||||||
|
table.insert(diff, { oldTable[i], nil }) -- Remaining Removals
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
while j <= #newTable do
|
||||||
|
table.insert(diff, { nil, newTable[j] }) -- Remaining Additions
|
||||||
|
j += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
Timer.stop()
|
||||||
|
return diff
|
||||||
|
end
|
||||||
|
|
||||||
|
function Array:didUpdate(previousProps)
|
||||||
|
if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Array:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local diff = self.state.diff
|
||||||
|
local lines = table.create(#diff)
|
||||||
|
|
||||||
|
for i, element in diff do
|
||||||
|
local oldValue = element[1]
|
||||||
|
local newValue = element[2]
|
||||||
|
|
||||||
|
local patchType = if oldValue == nil then "Add" elseif newValue == nil then "Remove" else "Remain"
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
lines,
|
||||||
|
e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
|
||||||
|
BackgroundColor3 = if patchType == "Remain" then theme.Diff.Row else theme.Diff[patchType],
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
LayoutOrder = i,
|
||||||
|
}, {
|
||||||
|
DiffIcon = if patchType ~= "Remain"
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Diff[patchType],
|
||||||
|
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 15, 0, 15),
|
||||||
|
Position = UDim2.new(0, 7, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
Old = e("Frame", {
|
||||||
|
Size = UDim2.new(0.5, -30, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Display = if oldValue ~= nil
|
||||||
|
then e(DisplayValue, {
|
||||||
|
value = oldValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
New = e("Frame", {
|
||||||
|
Size = UDim2.new(0.5, -10, 1, 0),
|
||||||
|
Position = UDim2.new(0.5, 5, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Display = if newValue ~= nil
|
||||||
|
then e(DisplayValue, {
|
||||||
|
value = newValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment({
|
||||||
|
Headers = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
BackgroundTransparency = self.props.transparency:map(function(t)
|
||||||
|
return 0.95 + (0.05 * t)
|
||||||
|
end),
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
}, {
|
||||||
|
ColumnA = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.5, -30, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "Old",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
ColumnB = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.5, -10, 1, 0),
|
||||||
|
Position = UDim2.new(0.5, 5, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "New",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
Separator = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
|
Position = UDim2.new(0, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 0,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
KeyValues = e(ScrollingFrame, {
|
||||||
|
position = UDim2.new(0, 1, 0, 25),
|
||||||
|
size = UDim2.new(1, -2, 1, -27),
|
||||||
|
scrollingDirection = Enum.ScrollingDirection.Y,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||||
|
}),
|
||||||
|
Lines = Roact.createFragment(lines),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Array
|
||||||
211
plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Timer = require(Plugin.Timer)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Dictionary = Roact.Component:extend("Dictionary")
|
||||||
|
|
||||||
|
function Dictionary:init()
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dictionary:calculateDiff()
|
||||||
|
Timer.start("Dictionary:calculateDiff")
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
|
||||||
|
-- Diff the two tables and find the added keys, removed keys, and changed keys
|
||||||
|
local diff = {}
|
||||||
|
|
||||||
|
for key, oldValue in oldTable do
|
||||||
|
local newValue = newTable[key]
|
||||||
|
if newValue == nil then
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Remove",
|
||||||
|
})
|
||||||
|
elseif newValue ~= oldValue then
|
||||||
|
-- Note: should this do some sort of deep comparison for various types?
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Edit",
|
||||||
|
})
|
||||||
|
else
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Remain",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for key in newTable do
|
||||||
|
if oldTable[key] == nil then
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Add",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(diff, function(a, b)
|
||||||
|
return a.key < b.key
|
||||||
|
end)
|
||||||
|
|
||||||
|
Timer.stop()
|
||||||
|
return diff
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dictionary:didUpdate(previousProps)
|
||||||
|
if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dictionary:render()
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
local diff = self.state.diff
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local lines = table.create(#diff)
|
||||||
|
for order, line in diff do
|
||||||
|
local key = line.key
|
||||||
|
local oldValue = oldTable[key]
|
||||||
|
local newValue = newTable[key]
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
lines,
|
||||||
|
e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
LayoutOrder = order,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
||||||
|
BackgroundColor3 = if line.patchType == "Remain"
|
||||||
|
then theme.Diff.Row
|
||||||
|
else theme.Diff[line.patchType],
|
||||||
|
}, {
|
||||||
|
DiffIcon = if line.patchType ~= "Remain"
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Diff[line.patchType],
|
||||||
|
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 15, 0, 15),
|
||||||
|
Position = UDim2.new(0, 7, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
KeyName = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.3, -15, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = key,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
OldValue = e("Frame", {
|
||||||
|
Size = UDim2.new(0.35, -7, 1, 0),
|
||||||
|
Position = UDim2.new(0.3, 15, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = oldValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
NewValue = e("Frame", {
|
||||||
|
Size = UDim2.new(0.35, -8, 1, 0),
|
||||||
|
Position = UDim2.new(0.65, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = newValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment({
|
||||||
|
Headers = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
BackgroundTransparency = self.props.transparency:map(function(t)
|
||||||
|
return 0.95 + (0.05 * t)
|
||||||
|
end),
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
}, {
|
||||||
|
ColumnA = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.3, -15, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "Key",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
ColumnB = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.35, -7, 1, 0),
|
||||||
|
Position = UDim2.new(0.3, 15, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "Old",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
ColumnC = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.35, -8, 1, 0),
|
||||||
|
Position = UDim2.new(0.65, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "New",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
Separator = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
|
Position = UDim2.new(0, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 0,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
KeyValues = e(ScrollingFrame, {
|
||||||
|
position = UDim2.new(0, 1, 0, 25),
|
||||||
|
size = UDim2.new(1, -2, 1, -27),
|
||||||
|
scrollingDirection = Enum.ScrollingDirection.Y,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||||
|
}),
|
||||||
|
Lines = Roact.createFragment(lines),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Dictionary
|
||||||
48
plugin/src/App/Components/TableDiffVisualizer/init.lua
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local Array = require(script:FindFirstChild("Array"))
|
||||||
|
local Dictionary = require(script:FindFirstChild("Dictionary"))
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local TableDiffVisualizer = Roact.Component:extend("TableDiffVisualizer")
|
||||||
|
|
||||||
|
function TableDiffVisualizer:render()
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
|
||||||
|
-- Ensure we're diffing tables, not mixing types
|
||||||
|
if type(oldTable) ~= "table" then
|
||||||
|
oldTable = {}
|
||||||
|
end
|
||||||
|
if type(newTable) ~= "table" then
|
||||||
|
newTable = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local isArray = next(newTable) == 1 or next(oldTable) == 1
|
||||||
|
|
||||||
|
return e(BorderedContainer, {
|
||||||
|
size = self.props.size,
|
||||||
|
position = self.props.position,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Content = if isArray
|
||||||
|
then e(Array, {
|
||||||
|
oldTable = oldTable,
|
||||||
|
newTable = newTable,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
})
|
||||||
|
else e(Dictionary, {
|
||||||
|
oldTable = oldTable,
|
||||||
|
newTable = newTable,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return TableDiffVisualizer
|
||||||
56
plugin/src/App/Components/Tag.lua
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
|
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
return function(props)
|
||||||
|
return e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBackground,
|
||||||
|
color = props.color,
|
||||||
|
transparency = props.transparency:map(function(transparency)
|
||||||
|
return 0.9 + (0.1 * transparency)
|
||||||
|
end),
|
||||||
|
layoutOrder = props.layoutOrder,
|
||||||
|
position = props.position,
|
||||||
|
anchorPoint = props.anchorPoint,
|
||||||
|
size = UDim2.new(0, 0, 0, 16),
|
||||||
|
automaticSize = Enum.AutomaticSize.X,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 4),
|
||||||
|
PaddingRight = UDim.new(0, 4),
|
||||||
|
PaddingTop = UDim.new(0, 2),
|
||||||
|
PaddingBottom = UDim.new(0, 2),
|
||||||
|
}),
|
||||||
|
Icon = if props.icon
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Size = UDim2.new(0, 12, 0, 12),
|
||||||
|
Position = UDim2.new(0, 0, 0.5, 0),
|
||||||
|
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,
|
||||||
|
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
|
||||||
@@ -41,10 +41,8 @@ end
|
|||||||
|
|
||||||
function TextButton:render()
|
function TextButton:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local textSize = TextService:GetTextSize(
|
local textSize =
|
||||||
self.props.text, 18, Enum.Font.GothamSemibold,
|
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
|
||||||
Vector2.new(math.huge, math.huge)
|
|
||||||
)
|
|
||||||
|
|
||||||
local style = self.props.style
|
local style = self.props.style
|
||||||
|
|
||||||
@@ -85,7 +83,7 @@ function TextButton:render()
|
|||||||
|
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamSemibold,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 18,
|
TextSize = 18,
|
||||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
@@ -124,7 +122,11 @@ function TextButton:render()
|
|||||||
|
|
||||||
Background = style == "Solid" and e(SlicedImage, {
|
Background = style == "Solid" and e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BackgroundColor, theme.Disabled.BackgroundColor),
|
color = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
theme.Enabled.BackgroundColor,
|
||||||
|
theme.Disabled.BackgroundColor
|
||||||
|
),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
|||||||
107
plugin/src/App/Components/TextInput.lua
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
|
||||||
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
|
|
||||||
|
local SPRING_PROPS = {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local TextInput = Roact.Component:extend("TextInput")
|
||||||
|
|
||||||
|
function TextInput:init()
|
||||||
|
self.motor = Flipper.GroupMotor.new({
|
||||||
|
hover = 0,
|
||||||
|
enabled = self.props.enabled and 1 or 0,
|
||||||
|
})
|
||||||
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextInput:didUpdate(lastProps)
|
||||||
|
if lastProps.enabled ~= self.props.enabled then
|
||||||
|
self.motor:setGoal({
|
||||||
|
enabled = Flipper.Spring.new(self.props.enabled and 1 or 0),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextInput:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
theme = theme.TextInput
|
||||||
|
|
||||||
|
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||||
|
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||||
|
|
||||||
|
return e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBorder,
|
||||||
|
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
||||||
|
position = self.props.position,
|
||||||
|
layoutOrder = self.props.layoutOrder,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
}, {
|
||||||
|
HoverOverlay = e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBackground,
|
||||||
|
color = theme.ActionFillColor,
|
||||||
|
transparency = Roact.joinBindings({
|
||||||
|
hover = bindingHover:map(function(value)
|
||||||
|
return 1 - value
|
||||||
|
end),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}):map(function(values)
|
||||||
|
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
||||||
|
end),
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
zIndex = -1,
|
||||||
|
}),
|
||||||
|
Input = e("TextBox", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
Text = self.props.text,
|
||||||
|
PlaceholderText = self.props.placeholder,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
|
||||||
|
PlaceholderColor3 = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
theme.Disabled.PlaceholderColor,
|
||||||
|
theme.Enabled.PlaceholderColor
|
||||||
|
),
|
||||||
|
TextSize = 18,
|
||||||
|
TextEditable = self.props.enabled,
|
||||||
|
ClearTextOnFocus = self.props.clearTextOnFocus,
|
||||||
|
|
||||||
|
[Roact.Event.MouseEnter] = function()
|
||||||
|
self.motor:setGoal({
|
||||||
|
hover = Flipper.Spring.new(1, SPRING_PROPS),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
[Roact.Event.MouseLeave] = function()
|
||||||
|
self.motor:setGoal({
|
||||||
|
hover = Flipper.Spring.new(0, SPRING_PROPS),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
[Roact.Event.FocusLost] = function(rbx)
|
||||||
|
self.props.onEntered(rbx.Text)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
Children = Roact.createFragment(self.props[Roact.Children]),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return TextInput
|
||||||
@@ -22,12 +22,16 @@ local TooltipContext = Roact.createContext({})
|
|||||||
|
|
||||||
local function Popup(props)
|
local function Popup(props)
|
||||||
local textSize = TextService:GetTextSize(
|
local textSize = TextService:GetTextSize(
|
||||||
props.Text, 16, Enum.Font.GothamMedium, Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
props.Text,
|
||||||
|
16,
|
||||||
|
Enum.Font.GothamMedium,
|
||||||
|
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
||||||
) + TEXT_PADDING + (Vector2.one * 2)
|
) + TEXT_PADDING + (Vector2.one * 2)
|
||||||
|
|
||||||
local trigger = props.Trigger:getValue()
|
local trigger = props.Trigger:getValue()
|
||||||
|
|
||||||
local spaceBelow = props.parentSize.Y - (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
local spaceBelow = props.parentSize.Y
|
||||||
|
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||||
local spaceAbove = trigger.AbsolutePosition.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
|
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||||
@@ -39,7 +43,10 @@ local function Popup(props)
|
|||||||
if displayAbove then
|
if displayAbove then
|
||||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
||||||
else
|
else
|
||||||
Y = math.min(trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP, props.parentSize.Y - textSize.Y)
|
Y = math.min(
|
||||||
|
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||||
|
props.parentSize.Y - textSize.Y
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
@@ -64,17 +71,9 @@ local function Popup(props)
|
|||||||
|
|
||||||
Tail = e("ImageLabel", {
|
Tail = e("ImageLabel", {
|
||||||
ZIndex = 100,
|
ZIndex = 100,
|
||||||
Position =
|
Position = if displayAbove
|
||||||
if displayAbove then
|
then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
|
||||||
UDim2.new(
|
else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||||
0, math.clamp(props.Position.X - X, 6, textSize.X-6),
|
|
||||||
1, -1
|
|
||||||
)
|
|
||||||
else
|
|
||||||
UDim2.new(
|
|
||||||
0, math.clamp(props.Position.X - X, 6, textSize.X-6),
|
|
||||||
0, -TAIL_SIZE+1
|
|
||||||
),
|
|
||||||
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
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,
|
||||||
@@ -90,7 +89,7 @@ local function Popup(props)
|
|||||||
ImageColor3 = theme.BorderedContainer.BorderColor,
|
ImageColor3 = theme.BorderedContainer.BorderColor,
|
||||||
ImageTransparency = props.transparency,
|
ImageTransparency = props.transparency,
|
||||||
}),
|
}),
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -164,47 +163,93 @@ local Trigger = Roact.Component:extend("TooltipTrigger")
|
|||||||
function Trigger:init()
|
function Trigger:init()
|
||||||
self.id = HttpService:GenerateGUID(false)
|
self.id = HttpService:GenerateGUID(false)
|
||||||
self.ref = Roact.createRef()
|
self.ref = Roact.createRef()
|
||||||
self.mousePos = Vector2.zero
|
self.showingPopup = false
|
||||||
|
|
||||||
self.destroy = function()
|
self.destroy = function()
|
||||||
self.props.context.removeTip(self.id)
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Trigger:willUnmount()
|
function Trigger:willUnmount()
|
||||||
if self.showDelayThread then
|
if self.showDelayThread then
|
||||||
task.cancel(self.showDelayThread)
|
pcall(task.cancel, self.showDelayThread)
|
||||||
end
|
end
|
||||||
if self.destroy then
|
if self.destroy then
|
||||||
self.destroy()
|
self.destroy()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Trigger:didUpdate(prevProps)
|
||||||
|
if prevProps.text ~= self.props.text then
|
||||||
|
-- Any existing popup is now invalid
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
|
||||||
|
-- Let the new text propagate
|
||||||
|
self:managePopup()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:isHovering()
|
||||||
|
local rbx = self.ref.current
|
||||||
|
if rbx then
|
||||||
|
return rbx.GuiState == Enum.GuiState.Hover
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:getMousePos()
|
||||||
|
local rbx = self.ref.current
|
||||||
|
if rbx then
|
||||||
|
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
|
||||||
|
if widget then
|
||||||
|
return widget:GetRelativeMousePosition()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return Vector2.zero
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:managePopup()
|
||||||
|
if self:isHovering() then
|
||||||
|
if self.showingPopup or self.showDelayThread then
|
||||||
|
-- Don't duplicate popups
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.showDelayThread = task.delay(DELAY, function()
|
||||||
|
self.props.context.addTip(self.id, {
|
||||||
|
Text = self.props.text,
|
||||||
|
Position = self:getMousePos(),
|
||||||
|
Trigger = self.ref,
|
||||||
|
})
|
||||||
|
self.showDelayThread = nil
|
||||||
|
self.showingPopup = true
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
if self.showDelayThread then
|
||||||
|
pcall(task.cancel, self.showDelayThread)
|
||||||
|
self.showDelayThread = nil
|
||||||
|
end
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Trigger:render()
|
function Trigger:render()
|
||||||
|
local function recalculate()
|
||||||
|
self:managePopup()
|
||||||
|
end
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
Size = UDim2.fromScale(1, 1),
|
Size = UDim2.fromScale(1, 1),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
ZIndex = self.props.zIndex or 100,
|
ZIndex = self.props.zIndex or 100,
|
||||||
[Roact.Ref] = self.ref,
|
[Roact.Ref] = self.ref,
|
||||||
|
|
||||||
[Roact.Event.MouseMoved] = function(_rbx, x, y)
|
[Roact.Change.GuiState] = recalculate,
|
||||||
self.mousePos = Vector2.new(x, y)
|
[Roact.Change.AbsolutePosition] = recalculate,
|
||||||
end,
|
[Roact.Change.AbsoluteSize] = recalculate,
|
||||||
[Roact.Event.MouseEnter] = function()
|
|
||||||
self.showDelayThread = task.delay(DELAY, function()
|
|
||||||
self.props.context.addTip(self.id, {
|
|
||||||
Text = self.props.text,
|
|
||||||
Position = self.mousePos,
|
|
||||||
Trigger = self.ref,
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
[Roact.Event.MouseLeave] = function()
|
|
||||||
if self.showDelayThread then
|
|
||||||
task.cancel(self.showDelayThread)
|
|
||||||
end
|
|
||||||
self.props.context.removeTip(self.id)
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ function TouchRipple:init()
|
|||||||
})
|
})
|
||||||
self.binding = bindingUtil.fromMotor(self.motor)
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
|
|
||||||
self.position, self.setPosition = Roact.createBinding(
|
self.position, self.setPosition = Roact.createBinding(Vector2.new(0, 0))
|
||||||
Vector2.new(0, 0)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function TouchRipple:reset()
|
function TouchRipple:reset()
|
||||||
@@ -43,10 +41,7 @@ function TouchRipple:calculateRadius(position)
|
|||||||
local container = self.ref.current
|
local container = self.ref.current
|
||||||
|
|
||||||
if container then
|
if container then
|
||||||
local corner = Vector2.new(
|
local corner = Vector2.new(math.floor((1 - position.X) + 0.5), math.floor((1 - position.Y) + 0.5))
|
||||||
math.floor((1 - position.X) + 0.5),
|
|
||||||
math.floor((1 - position.Y) + 0.5)
|
|
||||||
)
|
|
||||||
|
|
||||||
local size = container.AbsoluteSize
|
local size = container.AbsoluteSize
|
||||||
local ratio = size / math.min(size.X, size.Y)
|
local ratio = size / math.min(size.X, size.Y)
|
||||||
@@ -93,10 +88,7 @@ function TouchRipple:render()
|
|||||||
input:GetPropertyChangedSignal("UserInputState"):Connect(function()
|
input:GetPropertyChangedSignal("UserInputState"):Connect(function()
|
||||||
local userInputState = input.UserInputState
|
local userInputState = input.UserInputState
|
||||||
|
|
||||||
if
|
if userInputState == Enum.UserInputState.Cancel or userInputState == Enum.UserInputState.End then
|
||||||
userInputState == Enum.UserInputState.Cancel
|
|
||||||
or userInputState == Enum.UserInputState.End
|
|
||||||
then
|
|
||||||
self.motor:setGoal({
|
self.motor:setGoal({
|
||||||
opacity = Flipper.Spring.new(0, {
|
opacity = Flipper.Spring.new(0, {
|
||||||
frequency = 5,
|
frequency = 5,
|
||||||
@@ -127,8 +119,10 @@ function TouchRipple:render()
|
|||||||
local containerAspect = containerSize.X / containerSize.Y
|
local containerAspect = containerSize.X / containerSize.Y
|
||||||
|
|
||||||
return UDim2.new(
|
return UDim2.new(
|
||||||
currentSize / math.max(containerAspect, 1), 0,
|
currentSize / math.max(containerAspect, 1),
|
||||||
currentSize * math.min(containerAspect, 1), 0
|
0,
|
||||||
|
currentSize * math.min(containerAspect, 1),
|
||||||
|
0
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end),
|
end),
|
||||||
|
|||||||
@@ -99,18 +99,30 @@ function VirtualScroller:refresh()
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function VirtualScroller:didUpdate(previousProps)
|
||||||
|
if self.props.count ~= previousProps.count then
|
||||||
|
-- Items have changed, so we need to refresh
|
||||||
|
self:refresh()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function VirtualScroller:render()
|
function VirtualScroller:render()
|
||||||
local props, state = self.props, self.state
|
local props, state = self.props, self.state
|
||||||
|
|
||||||
local items = {}
|
local items = {}
|
||||||
for i = state.Start, state.End do
|
for i = state.Start, state.End do
|
||||||
|
local content = props.render(i)
|
||||||
|
if content == nil then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
items["Item" .. i] = e("Frame", {
|
items["Item" .. i] = e("Frame", {
|
||||||
LayoutOrder = i,
|
LayoutOrder = i,
|
||||||
Size = props.getHeightBinding(i):map(function(height)
|
Size = props.getHeightBinding(i):map(function(height)
|
||||||
return UDim2.new(1, 0, 0, height)
|
return UDim2.new(1, 0, 0, height)
|
||||||
end),
|
end),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, props.render(i))
|
}, content)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
@@ -119,8 +131,8 @@ function VirtualScroller:render()
|
|||||||
Position = props.position,
|
Position = props.position,
|
||||||
AnchorPoint = props.anchorPoint,
|
AnchorPoint = props.anchorPoint,
|
||||||
BackgroundTransparency = props.backgroundTransparency or 1,
|
BackgroundTransparency = props.backgroundTransparency or 1,
|
||||||
BackgroundColor3 = props.backgroundColor3,
|
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
|
||||||
BorderColor3 = props.borderColor3,
|
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(0, s)
|
||||||
end),
|
end),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Flipper = require(Packages.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
|
||||||
local bindingUtil = require(script.Parent.bindingUtil)
|
local bindingUtil = require(script.Parent.bindingUtil)
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ local Theme = require(Plugin.App.Theme)
|
|||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
|
|
||||||
local baseClock = DateTime.now().UnixTimestampMillis
|
local baseClock = DateTime.now().UnixTimestampMillis
|
||||||
|
|
||||||
@@ -28,30 +30,24 @@ function Notification:init()
|
|||||||
self.lifetime = self.props.timeout
|
self.lifetime = self.props.timeout
|
||||||
|
|
||||||
self.motor:onStep(function(value)
|
self.motor:onStep(function(value)
|
||||||
if value <= 0 then
|
if value <= 0 and self.props.onClose then
|
||||||
if self.props.onClose then
|
|
||||||
self.props.onClose()
|
self.props.onClose()
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:dismiss()
|
function Notification:dismiss()
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(0, {
|
||||||
Flipper.Spring.new(0, {
|
|
||||||
frequency = 5,
|
frequency = 5,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Notification:didMount()
|
function Notification:didMount()
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(1, {
|
||||||
Flipper.Spring.new(1, {
|
|
||||||
frequency = 3,
|
frequency = 3,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
self.props.soundPlayer:play(Assets.Sounds.Notification)
|
self.props.soundPlayer:play(Assets.Sounds.Notification)
|
||||||
|
|
||||||
@@ -86,23 +82,51 @@ function Notification:willUnmount()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Notification:render()
|
function Notification:render()
|
||||||
local time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
|
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(
|
|
||||||
self.props.text,
|
|
||||||
15,
|
|
||||||
Enum.Font.GothamSemibold,
|
|
||||||
Vector2.new(350, 700)
|
|
||||||
)
|
|
||||||
|
|
||||||
local transparency = self.binding:map(function(value)
|
local transparency = self.binding:map(function(value)
|
||||||
return 1 - value
|
return 1 - value
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
|
||||||
|
|
||||||
|
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()
|
||||||
|
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(
|
||||||
|
action.text,
|
||||||
|
18,
|
||||||
|
Enum.Font.GothamMedium,
|
||||||
|
Vector2.new(math.huge, math.huge)
|
||||||
|
).X + 30
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
buttonsX += (count - 1) * 5
|
||||||
|
end
|
||||||
|
|
||||||
|
local paddingY, logoSize = 20, 32
|
||||||
|
local actionsY = if self.props.actions then 35 else 0
|
||||||
|
local contentX = math.max(textBounds.X, buttonsX)
|
||||||
|
|
||||||
local size = self.binding:map(function(value)
|
local size = self.binding:map(function(value)
|
||||||
return UDim2.fromOffset(
|
return UDim2.fromOffset(
|
||||||
(35+40+textBounds.X)*value,
|
(35 + 40 + contentX) * value,
|
||||||
math.max(14+20+textBounds.Y, 32+20)
|
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -122,22 +146,22 @@ function Notification:render()
|
|||||||
transparency = transparency,
|
transparency = transparency,
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
TextContainer = e("Frame", {
|
Contents = e("Frame", {
|
||||||
Size = UDim2.new(0, 35+textBounds.X, 1, -20),
|
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
||||||
Position = UDim2.new(0, 0, 0, 10),
|
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, 32, 0, 32),
|
Size = UDim2.new(0, logoSize, 0, logoSize),
|
||||||
Position = UDim2.new(0, 0, 0.5, 0),
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
AnchorPoint = Vector2.new(0, 0.5),
|
AnchorPoint = Vector2.new(0, 0),
|
||||||
}),
|
}),
|
||||||
Info = e("TextLabel", {
|
Info = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamSemibold,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 15,
|
TextSize = 15,
|
||||||
TextColor3 = theme.Notification.InfoColor,
|
TextColor3 = theme.Notification.InfoColor,
|
||||||
TextTransparency = transparency,
|
TextTransparency = transparency,
|
||||||
@@ -150,27 +174,30 @@ function Notification:render()
|
|||||||
LayoutOrder = 1,
|
LayoutOrder = 1,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
Time = e("TextLabel", {
|
Actions = if self.props.actions
|
||||||
Text = time:FormatLocalTime("LTS", "en-us"),
|
then e("Frame", {
|
||||||
Font = Enum.Font.Code,
|
Size = UDim2.new(1, -40, 0, 35),
|
||||||
TextSize = 12,
|
Position = UDim2.new(1, 0, 1, 0),
|
||||||
TextColor3 = theme.Notification.InfoColor,
|
AnchorPoint = Vector2.new(1, 1),
|
||||||
TextTransparency = transparency,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
|
|
||||||
Size = UDim2.new(1, -35, 0, 14),
|
|
||||||
Position = UDim2.new(0, 35, 1, -14),
|
|
||||||
|
|
||||||
LayoutOrder = 1,
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
}),
|
}),
|
||||||
|
Buttons = Roact.createFragment(actionButtons),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
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),
|
||||||
}),
|
}),
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -180,15 +207,16 @@ local Notifications = Roact.Component:extend("Notifications")
|
|||||||
function Notifications:render()
|
function Notifications:render()
|
||||||
local notifs = {}
|
local notifs = {}
|
||||||
|
|
||||||
for index, notif in ipairs(self.props.notifications) do
|
for id, notif in self.props.notifications do
|
||||||
notifs[notif] = e(Notification, {
|
notifs["NotifID_" .. id] = e(Notification, {
|
||||||
soundPlayer = self.props.soundPlayer,
|
soundPlayer = self.props.soundPlayer,
|
||||||
text = notif.text,
|
text = notif.text,
|
||||||
timestamp = notif.timestamp,
|
timestamp = notif.timestamp,
|
||||||
timeout = notif.timeout,
|
timeout = notif.timeout,
|
||||||
|
actions = notif.actions,
|
||||||
layoutOrder = (notif.timestamp - baseClock),
|
layoutOrder = (notif.timestamp - baseClock),
|
||||||
onClose = function()
|
onClose = function()
|
||||||
self.props.onClose(index)
|
self.props.onClose(id)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ local Page = Roact.Component:extend("Page")
|
|||||||
|
|
||||||
function Page:init()
|
function Page:init()
|
||||||
self:setState({
|
self:setState({
|
||||||
rendered = self.props.active
|
rendered = self.props.active,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0)
|
self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0)
|
||||||
@@ -51,20 +51,21 @@ function Page:render()
|
|||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
Component = e(self.props.component, Dictionary.merge(self.props, {
|
Component = e(
|
||||||
|
self.props.component,
|
||||||
|
Dictionary.merge(self.props, {
|
||||||
transparency = transparency,
|
transparency = transparency,
|
||||||
}))
|
})
|
||||||
|
),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Page:didUpdate(lastProps)
|
function Page:didUpdate(lastProps)
|
||||||
if self.props.active ~= lastProps.active then
|
if self.props.active ~= lastProps.active then
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, {
|
||||||
Flipper.Spring.new(self.props.active and 1 or 0, {
|
|
||||||
frequency = 6,
|
frequency = 6,
|
||||||
dampingRatio = 1,
|
dampingRatio = 1,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
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
|
||||||
|
|
||||||
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)
|
||||||
local Header = require(Plugin.App.Components.Header)
|
|
||||||
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||||
|
local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -21,26 +22,52 @@ local ConfirmingPage = Roact.Component:extend("ConfirmingPage")
|
|||||||
function ConfirmingPage:init()
|
function ConfirmingPage:init()
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
||||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
patchTree = nil,
|
||||||
|
showingStringDiff = false,
|
||||||
|
oldString = "",
|
||||||
|
newString = "",
|
||||||
|
showingTableDiff = false,
|
||||||
|
oldTable = {},
|
||||||
|
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()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local pageContent = Roact.createFragment({
|
local pageContent = Roact.createFragment({
|
||||||
Header = e(Header, {
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
layoutOrder = 1,
|
|
||||||
}),
|
|
||||||
|
|
||||||
Title = e("TextLabel", {
|
Title = e("TextLabel", {
|
||||||
Text = string.format(
|
Text = string.format(
|
||||||
"Sync changes for project '%s':",
|
"Sync changes for project '%s':",
|
||||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||||
),
|
),
|
||||||
LayoutOrder = 2,
|
|
||||||
Font = Enum.Font.Gotham,
|
Font = Enum.Font.Gotham,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
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, 20),
|
||||||
@@ -48,13 +75,26 @@ function ConfirmingPage:render()
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
PatchVisualizer = e(PatchVisualizer, {
|
PatchVisualizer = e(PatchVisualizer, {
|
||||||
size = UDim2.new(1, 0, 1, -150),
|
size = UDim2.new(1, 0, 1, -100),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
columnVisibility = {true, true, true},
|
patchTree = self.state.patchTree,
|
||||||
patch = self.props.confirmData.patch,
|
|
||||||
instanceMap = self.props.confirmData.instanceMap,
|
showStringDiff = function(oldString: string, newString: string)
|
||||||
|
self:setState({
|
||||||
|
showingStringDiff = true,
|
||||||
|
oldString = oldString,
|
||||||
|
newString = newString,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
||||||
|
self:setState({
|
||||||
|
showingTableDiff = true,
|
||||||
|
oldTable = oldTable,
|
||||||
|
newTable = newTable,
|
||||||
|
})
|
||||||
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Buttons = e("Frame", {
|
Buttons = e("Frame", {
|
||||||
@@ -70,7 +110,7 @@ function ConfirmingPage:render()
|
|||||||
onClick = self.props.onAbort,
|
onClick = self.props.onAbort,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Stop the connection process"
|
text = "Stop the connection process",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -83,7 +123,7 @@ function ConfirmingPage:render()
|
|||||||
onClick = self.props.onReject,
|
onClick = self.props.onReject,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Push Studio changes to the Rojo server"
|
text = "Push Studio changes to the Rojo server",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
@@ -96,7 +136,7 @@ function ConfirmingPage:render()
|
|||||||
onClick = self.props.onAccept,
|
onClick = self.props.onAccept,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Pull Rojo server changes to Studio"
|
text = "Pull Rojo server changes to Studio",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -108,6 +148,11 @@ function ConfirmingPage:render()
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 8),
|
||||||
|
PaddingRight = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
Layout = e("UIListLayout", {
|
Layout = e("UIListLayout", {
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
@@ -116,9 +161,80 @@ function ConfirmingPage:render()
|
|||||||
Padding = UDim.new(0, 10),
|
Padding = UDim.new(0, 10),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
StringDiff = e(StudioPluginGui, {
|
||||||
PaddingLeft = UDim.new(0, 20),
|
id = "Rojo_ConfirmingStringDiff",
|
||||||
PaddingRight = UDim.new(0, 20),
|
title = "String diff",
|
||||||
|
active = self.state.showingStringDiff,
|
||||||
|
isEphemeral = true,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = true,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingStringDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(StringDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldString = self.state.oldString,
|
||||||
|
newString = self.state.newString,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
TableDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConfirmingTableDiff",
|
||||||
|
title = "Table diff",
|
||||||
|
active = self.state.showingTableDiff,
|
||||||
|
isEphemeral = true,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = true,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingTableDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(TableDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldTable = self.state.oldTable,
|
||||||
|
newTable = self.state.newTable,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -130,10 +246,10 @@ function ConfirmingPage:render()
|
|||||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||||
),
|
),
|
||||||
active = true,
|
active = true,
|
||||||
|
isEphemeral = true,
|
||||||
|
|
||||||
initDockState = Enum.InitialDockState.Float,
|
initDockState = Enum.InitialDockState.Float,
|
||||||
initEnabled = true,
|
overridePreviousState = false,
|
||||||
overridePreviousState = true,
|
|
||||||
floatingSize = Vector2.new(500, 350),
|
floatingSize = Vector2.new(500, 350),
|
||||||
minimumSize = Vector2.new(400, 250),
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
|||||||
@@ -3,80 +3,202 @@ local Plugin = Rojo.Plugin
|
|||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Flipper = require(Packages.Flipper)
|
|
||||||
|
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local timeUtil = require(Plugin.timeUtil)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
|
|
||||||
|
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||||
local Header = require(Plugin.App.Components.Header)
|
local Header = require(Plugin.App.Components.Header)
|
||||||
local IconButton = require(Plugin.App.Components.IconButton)
|
local IconButton = require(Plugin.App.Components.IconButton)
|
||||||
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||||
|
local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local AGE_UNITS = { {31556909, "year"}, {2629743, "month"}, {604800, "week"}, {86400, "day"}, {3600, "hour"}, {60, "minute"}, }
|
local ChangesViewer = Roact.Component:extend("ChangesViewer")
|
||||||
function timeSinceText(elapsed: number): string
|
|
||||||
if elapsed < 3 then
|
function ChangesViewer:init()
|
||||||
return "just now"
|
-- Hold onto the serve session during the lifecycle of this component
|
||||||
|
-- so that it can still render during the fade out after disconnecting
|
||||||
|
self.serveSession = self.props.serveSession
|
||||||
end
|
end
|
||||||
|
|
||||||
local ageText = string.format("%d seconds ago", elapsed)
|
function ChangesViewer:render()
|
||||||
|
if self.props.rendered == false or self.serveSession == nil or self.props.patchData == nil then
|
||||||
for _,UnitData in ipairs(AGE_UNITS) do
|
|
||||||
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
|
|
||||||
if elapsed > UnitSeconds then
|
|
||||||
local c = math.floor(elapsed/UnitSeconds)
|
|
||||||
ageText = string.format("%d %s%s ago", c, UnitName, c>1 and "s" or "")
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return ageText
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ChangesDrawer(props)
|
|
||||||
if props.rendered == false then
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local unapplied = PatchSet.countChanges(self.props.patchData.unapplied)
|
||||||
|
local applied = PatchSet.countChanges(self.props.patchData.patch) - unapplied
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e(BorderedContainer, {
|
return Roact.createFragment({
|
||||||
transparency = props.transparency,
|
Navbar = e("Frame", {
|
||||||
size = props.height:map(function(y)
|
Size = UDim2.new(1, 0, 0, 40),
|
||||||
return UDim2.new(1, 0, y, -180 * y)
|
BackgroundTransparency = 1,
|
||||||
end),
|
|
||||||
position = UDim2.new(0, 0, 1, 0),
|
|
||||||
anchorPoint = Vector2.new(0, 1),
|
|
||||||
layoutOrder = props.layoutOrder,
|
|
||||||
}, {
|
}, {
|
||||||
Close = e(IconButton, {
|
Close = e(IconButton, {
|
||||||
icon = Assets.Images.Icons.Close,
|
icon = Assets.Images.Icons.Close,
|
||||||
iconSize = 24,
|
iconSize = 24,
|
||||||
color = theme.ConnectionDetails.DisconnectColor,
|
color = theme.Settings.Navbar.BackButtonColor,
|
||||||
transparency = props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
position = UDim2.new(1, 0, 0, 0),
|
position = UDim2.new(0, 0, 0.5, 0),
|
||||||
anchorPoint = Vector2.new(1, 0),
|
anchorPoint = Vector2.new(0, 0.5),
|
||||||
|
|
||||||
onClick = props.onClose,
|
onClick = self.props.onBack,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Close the patch visualizer"
|
text = "Close",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
PatchVisualizer = e(PatchVisualizer, {
|
Title = e("TextLabel", {
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
Text = "Sync",
|
||||||
transparency = props.transparency,
|
Font = Enum.Font.GothamMedium,
|
||||||
layoutOrder = 3,
|
TextSize = 17,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
Size = UDim2.new(1, -40, 0, 20),
|
||||||
|
Position = UDim2.new(0, 40, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
|
||||||
columnVisibility = {true, false, true},
|
Subtitle = e("TextLabel", {
|
||||||
patch = props.patchInfo:getValue().patch,
|
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
||||||
instanceMap = props.serveSession.__instanceMap,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.SubTextColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
Size = UDim2.new(1, -40, 0, 16),
|
||||||
|
Position = UDim2.new(0, 40, 0, 20),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Info = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 10, 0, 24),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
Position = UDim2.new(1, -5, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0.5),
|
||||||
|
}, {
|
||||||
|
Tooltip = e(Tooltip.Trigger, {
|
||||||
|
text = `{applied} changes applied`
|
||||||
|
.. (if unapplied > 0 then `, {unapplied} changes failed` else ""),
|
||||||
|
}),
|
||||||
|
Content = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
|
||||||
|
StatusIcon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Image = if unapplied > 0
|
||||||
|
then Assets.Images.Icons.SyncWarning
|
||||||
|
else Assets.Images.Icons.SyncSuccess,
|
||||||
|
ImageColor3 = if unapplied > 0 then theme.Diff.Warning else theme.TextColor,
|
||||||
|
Size = UDim2.new(0, 24, 0, 24),
|
||||||
|
LayoutOrder = 10,
|
||||||
|
}),
|
||||||
|
StatusSpacer = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 6, 0, 4),
|
||||||
|
LayoutOrder = 9,
|
||||||
|
}),
|
||||||
|
AppliedIcon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Image = Assets.Images.Icons.Checkmark,
|
||||||
|
ImageColor3 = theme.TextColor,
|
||||||
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
AppliedText = e("TextLabel", {
|
||||||
|
Text = applied,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
Warnings = if unapplied > 0
|
||||||
|
then Roact.createFragment({
|
||||||
|
WarningsSpacer = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 4, 0, 4),
|
||||||
|
LayoutOrder = 3,
|
||||||
|
}),
|
||||||
|
UnappliedIcon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Image = Assets.Images.Icons.Exclamation,
|
||||||
|
ImageColor3 = theme.Diff.Warning,
|
||||||
|
Size = UDim2.new(0, 4, 0, 16),
|
||||||
|
LayoutOrder = 4,
|
||||||
|
}),
|
||||||
|
UnappliedText = e("TextLabel", {
|
||||||
|
Text = unapplied,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.Diff.Warning,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 5,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Divider = e("Frame", {
|
||||||
|
BackgroundColor3 = theme.Settings.DividerColor,
|
||||||
|
BackgroundTransparency = self.props.transparency,
|
||||||
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
|
Position = UDim2.new(0, 0, 1, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
}, {
|
||||||
|
Gradient = e("UIGradient", {
|
||||||
|
Transparency = NumberSequence.new({
|
||||||
|
NumberSequenceKeypoint.new(0, 1),
|
||||||
|
NumberSequenceKeypoint.new(0.1, 0),
|
||||||
|
NumberSequenceKeypoint.new(0.9, 0),
|
||||||
|
NumberSequenceKeypoint.new(1, 1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Patch = e(PatchVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -65),
|
||||||
|
position = UDim2.new(0, 5, 1, -5),
|
||||||
|
anchorPoint = Vector2.new(0, 1),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = self.props.layoutOrder,
|
||||||
|
|
||||||
|
patchTree = self.props.patchTree,
|
||||||
|
|
||||||
|
showStringDiff = self.props.showStringDiff,
|
||||||
|
showTableDiff = self.props.showTableDiff,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
@@ -91,7 +213,7 @@ local function ConnectionDetails(props)
|
|||||||
}, {
|
}, {
|
||||||
TextContainer = e("Frame", {
|
TextContainer = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
BackgroundTransparency = 1
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
ProjectName = e("TextLabel", {
|
ProjectName = e("TextLabel", {
|
||||||
Text = props.projectName,
|
Text = props.projectName,
|
||||||
@@ -129,22 +251,6 @@ local function ConnectionDetails(props)
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Disconnect = e(IconButton, {
|
|
||||||
icon = Assets.Images.Icons.Close,
|
|
||||||
iconSize = 24,
|
|
||||||
color = theme.ConnectionDetails.DisconnectColor,
|
|
||||||
transparency = props.transparency,
|
|
||||||
|
|
||||||
position = UDim2.new(1, 0, 0.5, 0),
|
|
||||||
anchorPoint = Vector2.new(1, 0.5),
|
|
||||||
|
|
||||||
onClick = props.onDisconnect,
|
|
||||||
}, {
|
|
||||||
Tip = e(Tooltip.Trigger, {
|
|
||||||
text = "Disconnect from the Rojo sync server"
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
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),
|
||||||
@@ -155,30 +261,80 @@ end
|
|||||||
|
|
||||||
local ConnectedPage = Roact.Component:extend("ConnectedPage")
|
local ConnectedPage = Roact.Component:extend("ConnectedPage")
|
||||||
|
|
||||||
function ConnectedPage:init()
|
function ConnectedPage:getChangeInfoText()
|
||||||
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
|
local patchData = self.props.patchData
|
||||||
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
|
if patchData == nil then
|
||||||
|
return ""
|
||||||
self.changeDrawerMotor:onStep(function(value)
|
end
|
||||||
local renderChanges = value > 0.05
|
return timeUtil.elapsedToText(DateTime.now().UnixTimestamp - patchData.timestamp)
|
||||||
|
|
||||||
self:setState(function(state)
|
|
||||||
if state.renderChanges == renderChanges then
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
function ConnectedPage:startChangeInfoTextUpdater()
|
||||||
renderChanges = renderChanges,
|
-- Cancel any existing updater
|
||||||
}
|
self:stopChangeInfoTextUpdater()
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
-- Start a new updater
|
||||||
|
self.changeInfoTextUpdater = task.defer(function()
|
||||||
|
while true do
|
||||||
|
self.setChangeInfoText(self:getChangeInfoText())
|
||||||
|
|
||||||
|
local elapsed = DateTime.now().UnixTimestamp - self.props.patchData.timestamp
|
||||||
|
local updateInterval = 1
|
||||||
|
|
||||||
|
-- Update timestamp text as frequently as currently needed
|
||||||
|
for _, UnitData in ipairs(timeUtil.AGE_UNITS) do
|
||||||
|
local UnitSeconds = UnitData[1]
|
||||||
|
if elapsed > UnitSeconds then
|
||||||
|
updateInterval = UnitSeconds
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task.wait(updateInterval)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:stopChangeInfoTextUpdater()
|
||||||
|
if self.changeInfoTextUpdater then
|
||||||
|
task.cancel(self.changeInfoTextUpdater)
|
||||||
|
self.changeInfoTextUpdater = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:init()
|
||||||
self:setState({
|
self:setState({
|
||||||
renderChanges = false,
|
renderChanges = false,
|
||||||
|
hoveringChangeInfo = false,
|
||||||
|
showingStringDiff = false,
|
||||||
|
oldString = "",
|
||||||
|
newString = "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||||
|
|
||||||
|
self:startChangeInfoTextUpdater()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:willUnmount()
|
||||||
|
self:stopChangeInfoTextUpdater()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ConnectedPage:didUpdate(previousProps)
|
||||||
|
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
|
||||||
|
-- New patch recieved
|
||||||
|
self:startChangeInfoTextUpdater()
|
||||||
|
self:setState({
|
||||||
|
showingStringDiff = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConnectedPage:render()
|
function ConnectedPage:render()
|
||||||
|
local syncWarning = self.props.patchData
|
||||||
|
and self.props.patchData.unapplied
|
||||||
|
and PatchSet.countChanges(self.props.patchData.unapplied) > 0
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return Roact.createFragment({
|
return Roact.createFragment({
|
||||||
Padding = e("UIPadding", {
|
Padding = e("UIPadding", {
|
||||||
@@ -193,9 +349,88 @@ function ConnectedPage:render()
|
|||||||
Padding = UDim.new(0, 10),
|
Padding = UDim.new(0, 10),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Heading = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 0, 32),
|
||||||
|
}, {
|
||||||
Header = e(Header, {
|
Header = e(Header, {
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 1,
|
}),
|
||||||
|
|
||||||
|
ChangeInfo = e("TextButton", {
|
||||||
|
Text = "",
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderedColor,
|
||||||
|
BackgroundTransparency = if self.state.hoveringChangeInfo then 0.7 else 1,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
Position = UDim2.new(1, -5, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0.5),
|
||||||
|
[Roact.Event.MouseEnter] = function()
|
||||||
|
self:setState({
|
||||||
|
hoveringChangeInfo = true,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
[Roact.Event.MouseLeave] = function()
|
||||||
|
self:setState({
|
||||||
|
hoveringChangeInfo = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
self:setState(function(prevState)
|
||||||
|
prevState = prevState or {}
|
||||||
|
return {
|
||||||
|
renderChanges = not prevState.renderChanges,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
Corner = e("UICorner", {
|
||||||
|
CornerRadius = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Tooltip = e(Tooltip.Trigger, {
|
||||||
|
text = if self.state.renderChanges then "Hide changes" else "View changes",
|
||||||
|
}),
|
||||||
|
Content = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 5),
|
||||||
|
PaddingRight = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Text = e("TextLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = self.changeInfoText,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Icon = e("ImageLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Image = if syncWarning
|
||||||
|
then Assets.Images.Icons.SyncWarning
|
||||||
|
else Assets.Images.Icons.SyncSuccess,
|
||||||
|
ImageColor3 = if syncWarning then theme.Diff.Warning else theme.Header.VersionColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
Size = UDim2.new(0, 24, 0, 24),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ConnectionDetails = e(ConnectionDetails, {
|
ConnectionDetails = e(ConnectionDetails, {
|
||||||
@@ -207,59 +442,173 @@ function ConnectedPage:render()
|
|||||||
onDisconnect = self.props.onDisconnect,
|
onDisconnect = self.props.onDisconnect,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ChangeInfo = e("TextButton", {
|
Buttons = e("Frame", {
|
||||||
Text = self.props.patchInfo:map(function(info)
|
Size = UDim2.new(1, 0, 0, 34),
|
||||||
local changes = PatchSet.countChanges(info.patch)
|
|
||||||
return string.format(
|
|
||||||
"<i>Synced %d change%s %s</i>",
|
|
||||||
changes,
|
|
||||||
changes == 1 and "" or "s",
|
|
||||||
timeSinceText(os.time() - info.timestamp)
|
|
||||||
)
|
|
||||||
end),
|
|
||||||
Font = Enum.Font.Gotham,
|
|
||||||
TextSize = 14,
|
|
||||||
TextWrapped = true,
|
|
||||||
RichText = true,
|
|
||||||
TextColor3 = theme.Header.VersionColor,
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
|
||||||
TextYAlignment = Enum.TextYAlignment.Top,
|
|
||||||
TextTransparency = self.props.transparency,
|
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 28),
|
|
||||||
|
|
||||||
LayoutOrder = 3,
|
LayoutOrder = 3,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
ZIndex = 2,
|
||||||
[Roact.Event.Activated] = function()
|
}, {
|
||||||
if self.state.renderChanges then
|
Settings = e(TextButton, {
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
text = "Settings",
|
||||||
frequency = 4,
|
style = "Bordered",
|
||||||
dampingRatio = 1,
|
transparency = self.props.transparency,
|
||||||
}))
|
layoutOrder = 1,
|
||||||
else
|
onClick = self.props.onNavigateSettings,
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
|
}, {
|
||||||
frequency = 3,
|
Tip = e(Tooltip.Trigger, {
|
||||||
dampingRatio = 1,
|
text = "View and modify plugin settings",
|
||||||
}))
|
}),
|
||||||
end
|
|
||||||
end,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ChangesDrawer = e(ChangesDrawer, {
|
Disconnect = e(TextButton, {
|
||||||
rendered = self.state.renderChanges,
|
text = "Disconnect",
|
||||||
|
style = "Solid",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
patchInfo = self.props.patchInfo,
|
layoutOrder = 2,
|
||||||
serveSession = self.props.serveSession,
|
onClick = self.props.onDisconnect,
|
||||||
height = self.changeDrawerHeight,
|
}, {
|
||||||
layoutOrder = 4,
|
Tip = e(Tooltip.Trigger, {
|
||||||
|
text = "Disconnect from the Rojo sync server",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 10),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
ChangesViewer = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ChangesViewer",
|
||||||
|
title = "View changes",
|
||||||
|
active = self.state.renderChanges,
|
||||||
|
isEphemeral = true,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = true,
|
||||||
|
floatingSize = Vector2.new(400, 500),
|
||||||
|
minimumSize = Vector2.new(300, 300),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
onClose = function()
|
onClose = function()
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
self:setState({
|
||||||
frequency = 4,
|
renderChanges = false,
|
||||||
dampingRatio = 1,
|
})
|
||||||
}))
|
|
||||||
end,
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Changes = e(ChangesViewer, {
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
rendered = self.state.renderChanges,
|
||||||
|
patchData = self.props.patchData,
|
||||||
|
patchTree = self.props.patchTree,
|
||||||
|
serveSession = self.props.serveSession,
|
||||||
|
showStringDiff = function(oldString: string, newString: string)
|
||||||
|
self:setState({
|
||||||
|
showingStringDiff = true,
|
||||||
|
oldString = oldString,
|
||||||
|
newString = newString,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
||||||
|
self:setState({
|
||||||
|
showingTableDiff = true,
|
||||||
|
oldTable = oldTable,
|
||||||
|
newTable = newTable,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
onBack = function()
|
||||||
|
self:setState({
|
||||||
|
renderChanges = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
StringDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConnectedStringDiff",
|
||||||
|
title = "String diff",
|
||||||
|
active = self.state.showingStringDiff,
|
||||||
|
isEphemeral = true,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = false,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingStringDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(StringDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldString = self.state.oldString,
|
||||||
|
newString = self.state.newString,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
TableDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConnectedTableDiff",
|
||||||
|
title = "Table diff",
|
||||||
|
active = self.state.showingTableDiff,
|
||||||
|
isEphemeral = true,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = false,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingTableDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(TableDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldTable = self.state.oldTable,
|
||||||
|
newTable = self.state.newTable,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ function Error:render()
|
|||||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||||
|
|
||||||
local textBounds = TextService:GetTextSize(
|
local textBounds = TextService:GetTextSize(
|
||||||
self.props.errorMessage, 16, Enum.Font.Code,
|
self.props.errorMessage,
|
||||||
|
16,
|
||||||
|
Enum.Font.Code,
|
||||||
Vector2.new(containerSize.X, math.huge)
|
Vector2.new(containerSize.X, math.huge)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,12 +62,13 @@ function Error:render()
|
|||||||
ErrorMessage = Theme.with(function(theme)
|
ErrorMessage = Theme.with(function(theme)
|
||||||
return e("TextBox", {
|
return e("TextBox", {
|
||||||
[Roact.Event.InputBegan] = function(rbx, input)
|
[Roact.Event.InputBegan] = function(rbx, input)
|
||||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
|
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
||||||
|
return
|
||||||
|
end
|
||||||
rbx.SelectionStart = 0
|
rbx.SelectionStart = 0
|
||||||
rbx.CursorPosition = #rbx.Text + 1
|
rbx.CursorPosition = #rbx.Text + 1
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
||||||
Text = self.props.errorMessage,
|
Text = self.props.errorMessage,
|
||||||
TextEditable = false,
|
TextEditable = false,
|
||||||
Font = Enum.Font.Code,
|
Font = Enum.Font.Code,
|
||||||
@@ -126,7 +129,7 @@ function ErrorPage:render()
|
|||||||
onClick = self.props.onClose,
|
onClick = self.props.onClose,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Dismiss message"
|
text = "Dismiss message",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ local function AddressEntry(props)
|
|||||||
if props.onHostChange ~= nil then
|
if props.onHostChange ~= nil then
|
||||||
props.onHostChange(object.Text)
|
props.onHostChange(object.Text)
|
||||||
end
|
end
|
||||||
end
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Port = e("TextBox", {
|
Port = e("TextBox", {
|
||||||
@@ -120,7 +120,7 @@ function NotConnectedPage:render()
|
|||||||
onClick = self.props.onNavigateSettings,
|
onClick = self.props.onNavigateSettings,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "View and modify plugin settings"
|
text = "View and modify plugin settings",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ function NotConnectedPage:render()
|
|||||||
onClick = self.props.onConnect,
|
onClick = self.props.onConnect,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Connect to a Rojo sync server"
|
text = "Connect to a Rojo sync server",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,23 @@ local Theme = require(Plugin.App.Theme)
|
|||||||
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)
|
||||||
local IconButton = require(Plugin.App.Components.IconButton)
|
local IconButton = require(Plugin.App.Components.IconButton)
|
||||||
|
local Tag = require(Plugin.App.Components.Tag)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local DIVIDER_FADE_SIZE = 0.1
|
local DIVIDER_FADE_SIZE = 0.1
|
||||||
|
local TAG_TYPES = {
|
||||||
|
unstable = {
|
||||||
|
text = "UNSTABLE",
|
||||||
|
icon = Assets.Images.Icons.Warning,
|
||||||
|
color = { "Settings", "Setting", "UnstableColor" },
|
||||||
|
},
|
||||||
|
debug = {
|
||||||
|
text = "DEBUG",
|
||||||
|
icon = Assets.Images.Icons.Debug,
|
||||||
|
color = { "Settings", "Setting", "DebugColor" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
||||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
||||||
@@ -27,11 +40,23 @@ local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
|||||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function getThemeColorFromPath(theme, path)
|
||||||
|
local color = theme
|
||||||
|
for _, key in path do
|
||||||
|
if color[key] == nil then
|
||||||
|
return theme.BrandColor
|
||||||
|
end
|
||||||
|
color = color[key]
|
||||||
|
end
|
||||||
|
return color
|
||||||
|
end
|
||||||
|
|
||||||
local Setting = Roact.Component:extend("Setting")
|
local Setting = Roact.Component:extend("Setting")
|
||||||
|
|
||||||
function Setting:init()
|
function Setting:init()
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
self.inputSize, self.setInputSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
setting = Settings:get(self.props.id),
|
setting = Settings:get(self.props.id),
|
||||||
@@ -50,94 +75,139 @@ end
|
|||||||
|
|
||||||
function Setting:render()
|
function Setting:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Settings
|
local settingsTheme = theme.Settings
|
||||||
|
|
||||||
return e("Frame", {
|
return e("Frame", {
|
||||||
Size = self.contentSize:map(function(value)
|
Size = self.contentSize:map(function(value)
|
||||||
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
|
return UDim2.new(1, 0, 0, value.Y + 20)
|
||||||
end),
|
end),
|
||||||
LayoutOrder = self.props.layoutOrder,
|
LayoutOrder = self.props.layoutOrder,
|
||||||
ZIndex = -self.props.layoutOrder,
|
ZIndex = -self.props.layoutOrder,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
Visible = self.props.visible,
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = function(object)
|
[Roact.Change.AbsoluteSize] = function(object)
|
||||||
self.setContainerSize(object.AbsoluteSize)
|
self.setContainerSize(object.AbsoluteSize)
|
||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
Input = if self.props.options ~= nil then
|
RightAligned = Roact.createElement("Frame", {
|
||||||
e(Dropdown, {
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 2),
|
||||||
|
[Roact.Change.AbsoluteContentSize] = function(rbx)
|
||||||
|
self.setInputSize(rbx.AbsoluteContentSize)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input = if self.props.input ~= nil
|
||||||
|
then self.props.input
|
||||||
|
elseif self.props.options ~= nil then e(Dropdown, {
|
||||||
|
locked = self.props.locked,
|
||||||
options = self.props.options,
|
options = self.props.options,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
position = UDim2.new(1, 0, 0.5, 0),
|
|
||||||
anchorPoint = Vector2.new(1, 0.5),
|
|
||||||
onClick = function(option)
|
onClick = function(option)
|
||||||
Settings:set(self.props.id, option)
|
Settings:set(self.props.id, option)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
else
|
else e(Checkbox, {
|
||||||
e(Checkbox, {
|
locked = self.props.locked,
|
||||||
active = self.state.setting,
|
active = self.state.setting,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
position = UDim2.new(1, 0, 0.5, 0),
|
|
||||||
anchorPoint = Vector2.new(1, 0.5),
|
|
||||||
onClick = function()
|
onClick = function()
|
||||||
local currentValue = Settings:get(self.props.id)
|
local currentValue = Settings:get(self.props.id)
|
||||||
Settings:set(self.props.id, not currentValue)
|
Settings:set(self.props.id, not currentValue)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Reset = if self.props.onReset then e(IconButton, {
|
Reset = if self.props.onReset
|
||||||
|
then e(IconButton, {
|
||||||
icon = Assets.Images.Icons.Reset,
|
icon = Assets.Images.Icons.Reset,
|
||||||
iconSize = 24,
|
iconSize = 24,
|
||||||
color = theme.BackButtonColor,
|
color = settingsTheme.BackButtonColor,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
visible = self.props.showReset,
|
visible = self.props.showReset,
|
||||||
|
layoutOrder = -1,
|
||||||
position = UDim2.new(1, -32 - (self.props.options ~= nil and 120 or 40), 0.5, 0),
|
|
||||||
anchorPoint = Vector2.new(0, 0.5),
|
|
||||||
|
|
||||||
onClick = self.props.onReset,
|
onClick = self.props.onReset,
|
||||||
}) else nil,
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
|
||||||
Text = e("Frame", {
|
Text = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}, {
|
}, {
|
||||||
|
Heading = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 16),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Tag = if self.props.tag and TAG_TYPES[self.props.tag]
|
||||||
|
then e(Tag, {
|
||||||
|
layoutOrder = 1,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
text = TAG_TYPES[self.props.tag].text,
|
||||||
|
icon = TAG_TYPES[self.props.tag].icon,
|
||||||
|
color = getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
Name = e("TextLabel", {
|
Name = e("TextLabel", {
|
||||||
Text = self.props.name,
|
Text = self.props.name,
|
||||||
Font = Enum.Font.GothamBold,
|
Font = Enum.Font.GothamBold,
|
||||||
TextSize = 17,
|
TextSize = 16,
|
||||||
TextColor3 = theme.Setting.NameColor,
|
TextColor3 = if self.props.tag and TAG_TYPES[self.props.tag]
|
||||||
|
then getThemeColorFromPath(theme, TAG_TYPES[self.props.tag].color)
|
||||||
|
else settingsTheme.Setting.NameColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
|
RichText = true,
|
||||||
|
|
||||||
Size = UDim2.new(1, 0, 0, 17),
|
Size = UDim2.new(1, 0, 0, 16),
|
||||||
|
|
||||||
LayoutOrder = 1,
|
LayoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
Description = e("TextLabel", {
|
Description = e("TextLabel", {
|
||||||
Text = self.props.description,
|
Text = self.props.description,
|
||||||
Font = Enum.Font.Gotham,
|
Font = Enum.Font.Gotham,
|
||||||
LineHeight = 1.2,
|
LineHeight = 1.2,
|
||||||
TextSize = 14,
|
TextSize = 14,
|
||||||
TextColor3 = theme.Setting.DescriptionColor,
|
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
||||||
TextXAlignment = Enum.TextXAlignment.Left,
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
TextWrapped = true,
|
TextWrapped = true,
|
||||||
|
RichText = true,
|
||||||
|
|
||||||
Size = self.containerSize:map(function(value)
|
Size = Roact.joinBindings({
|
||||||
local offset = (self.props.onReset and 34 or 0) + (self.props.options ~= nil and 120 or 40)
|
containerSize = self.containerSize,
|
||||||
|
inputSize = self.inputSize,
|
||||||
|
}):map(function(values)
|
||||||
|
local offset = values.inputSize.X + 5
|
||||||
local textBounds = getTextBounds(
|
local textBounds = getTextBounds(
|
||||||
self.props.description, 14, Enum.Font.Gotham, 1.2,
|
self.props.description,
|
||||||
Vector2.new(value.X - offset, math.huge)
|
14,
|
||||||
|
Enum.Font.Gotham,
|
||||||
|
1.2,
|
||||||
|
Vector2.new(values.containerSize.X - offset, math.huge)
|
||||||
)
|
)
|
||||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||||
end),
|
end),
|
||||||
|
|
||||||
LayoutOrder = 2,
|
LayoutOrder = 3,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -145,21 +215,16 @@ function Setting:render()
|
|||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
FillDirection = Enum.FillDirection.Vertical,
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
Padding = UDim.new(0, 6),
|
Padding = UDim.new(0, 5),
|
||||||
|
|
||||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||||
self.setContentSize(object.AbsoluteContentSize)
|
self.setContentSize(object.AbsoluteContentSize)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Padding = e("UIPadding", {
|
|
||||||
PaddingTop = UDim.new(0, 20),
|
|
||||||
PaddingBottom = UDim.new(0, 20),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Divider = e("Frame", {
|
Divider = e("Frame", {
|
||||||
BackgroundColor3 = theme.DividerColor,
|
BackgroundColor3 = settingsTheme.DividerColor,
|
||||||
BackgroundTransparency = self.props.transparency,
|
BackgroundTransparency = self.props.transparency,
|
||||||
Size = UDim2.new(1, 0, 0, 1),
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ local Theme = require(Plugin.App.Theme)
|
|||||||
local IconButton = require(Plugin.App.Components.IconButton)
|
local IconButton = require(Plugin.App.Components.IconButton)
|
||||||
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)
|
||||||
|
local TextInput = require(Plugin.App.Components.TextInput)
|
||||||
local Setting = require(script.Setting)
|
local Setting = require(script.Setting)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
@@ -25,6 +26,7 @@ local function invertTbl(tbl)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local invertedLevels = invertTbl(Log.Level)
|
local invertedLevels = invertTbl(Log.Level)
|
||||||
|
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
||||||
|
|
||||||
local function Navbar(props)
|
local function Navbar(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
@@ -47,7 +49,7 @@ local function Navbar(props)
|
|||||||
onClick = props.onBack,
|
onClick = props.onBack,
|
||||||
}, {
|
}, {
|
||||||
Tip = e(Tooltip.Trigger, {
|
Tip = e(Tooltip.Trigger, {
|
||||||
text = "Back"
|
text = "Back",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ local function Navbar(props)
|
|||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -73,34 +75,83 @@ function SettingsPage:init()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SettingsPage:render()
|
function SettingsPage:render()
|
||||||
|
local layoutOrder = 0
|
||||||
|
local function layoutIncrement()
|
||||||
|
layoutOrder += 1
|
||||||
|
return layoutOrder
|
||||||
|
end
|
||||||
|
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
theme = theme.Settings
|
theme = theme.Settings
|
||||||
|
|
||||||
return e(ScrollingFrame, {
|
return Roact.createFragment({
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
|
||||||
contentSize = self.contentSize,
|
|
||||||
transparency = self.props.transparency,
|
|
||||||
}, {
|
|
||||||
Navbar = e(Navbar, {
|
Navbar = e(Navbar, {
|
||||||
onBack = self.props.onBack,
|
onBack = self.props.onBack,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 0,
|
layoutOrder = layoutIncrement(),
|
||||||
}),
|
}),
|
||||||
|
Content = e(ScrollingFrame, {
|
||||||
OpenScriptsExternally = e(Setting, {
|
size = UDim2.new(1, 0, 1, -47),
|
||||||
id = "openScriptsExternally",
|
position = UDim2.new(0, 0, 0, 47),
|
||||||
name = "Open Scripts Externally",
|
contentSize = self.contentSize,
|
||||||
description = "Attempt to open scripts in an external editor",
|
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 1,
|
}, {
|
||||||
}),
|
|
||||||
|
|
||||||
ShowNotifications = e(Setting, {
|
ShowNotifications = e(Setting, {
|
||||||
id = "showNotifications",
|
id = "showNotifications",
|
||||||
name = "Show Notifications",
|
name = "Show Notifications",
|
||||||
description = "Popup notifications in viewport",
|
description = "Popup notifications in viewport",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 2,
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
SyncReminder = e(Setting, {
|
||||||
|
id = "syncReminder",
|
||||||
|
name = "Sync Reminder",
|
||||||
|
description = "Notify to sync when opening a place that has previously been synced",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
visible = Settings:getBinding("showNotifications"),
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
ConfirmationBehavior = e(Setting, {
|
||||||
|
id = "confirmationBehavior",
|
||||||
|
name = "Confirmation Behavior",
|
||||||
|
description = "When to prompt for confirmation before syncing",
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
layoutOrder = layoutIncrement(),
|
||||||
|
|
||||||
|
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),
|
||||||
|
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, {
|
PlaySounds = e(Setting, {
|
||||||
@@ -108,23 +159,63 @@ function SettingsPage:render()
|
|||||||
name = "Play Sounds",
|
name = "Play Sounds",
|
||||||
description = "Toggle sound effects",
|
description = "Toggle sound effects",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 3,
|
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, {
|
TwoWaySync = e(Setting, {
|
||||||
id = "twoWaySync",
|
id = "twoWaySync",
|
||||||
name = "Two-Way Sync",
|
name = "Two-Way Sync",
|
||||||
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
|
description = "Editing files in Studio will sync them into the filesystem",
|
||||||
|
locked = self.props.syncActive,
|
||||||
|
tag = "unstable",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 4,
|
layoutOrder = layoutIncrement(),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
LogLevel = e(Setting, {
|
LogLevel = e(Setting, {
|
||||||
id = "logLevel",
|
id = "logLevel",
|
||||||
name = "Log Level",
|
name = "Log Level",
|
||||||
description = "Plugin output verbosity level",
|
description = "Plugin output verbosity level",
|
||||||
|
tag = "debug",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 5,
|
layoutOrder = layoutIncrement(),
|
||||||
|
|
||||||
options = invertedLevels,
|
options = invertedLevels,
|
||||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||||
@@ -139,8 +230,18 @@ function SettingsPage:render()
|
|||||||
id = "typecheckingEnabled",
|
id = "typecheckingEnabled",
|
||||||
name = "Typechecking",
|
name = "Typechecking",
|
||||||
description = "Toggle typechecking on the API surface",
|
description = "Toggle typechecking on the API surface",
|
||||||
|
tag = "debug",
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
layoutOrder = 6,
|
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", {
|
Layout = e("UIListLayout", {
|
||||||
@@ -156,6 +257,7 @@ function SettingsPage:render()
|
|||||||
PaddingLeft = UDim.new(0, 20),
|
PaddingLeft = UDim.new(0, 20),
|
||||||
PaddingRight = UDim.new(0, 20),
|
PaddingRight = UDim.new(0, 20),
|
||||||
}),
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,212 +19,12 @@ local Rojo = script:FindFirstAncestor("Rojo")
|
|||||||
local Packages = Rojo.Packages
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Log = require(Packages.Log)
|
|
||||||
|
|
||||||
local strict = require(script.Parent.Parent.strict)
|
local strict = require(script.Parent.Parent.strict)
|
||||||
|
|
||||||
-- Copying hex colors back and forth from design programs is faster
|
local BRAND_COLOR = Color3.fromHex("E13835")
|
||||||
local function hexColor(decimal)
|
|
||||||
local red = bit32.band(bit32.rshift(decimal, 16), 2^8 - 1)
|
|
||||||
local green = bit32.band(bit32.rshift(decimal, 8), 2^8 - 1)
|
|
||||||
local blue = bit32.band(decimal, 2^8 - 1)
|
|
||||||
|
|
||||||
return Color3.fromRGB(red, green, blue)
|
local Context = Roact.createContext({})
|
||||||
end
|
|
||||||
|
|
||||||
local BRAND_COLOR = hexColor(0xE13835)
|
|
||||||
|
|
||||||
local lightTheme = strict("LightTheme", {
|
|
||||||
BackgroundColor = hexColor(0xFFFFFF),
|
|
||||||
Button = {
|
|
||||||
Solid = {
|
|
||||||
ActionFillColor = hexColor(0xFFFFFF),
|
|
||||||
ActionFillTransparency = 0.8,
|
|
||||||
Enabled = {
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
BackgroundColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Disabled = {
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
BackgroundColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Bordered = {
|
|
||||||
ActionFillColor = hexColor(0x000000),
|
|
||||||
ActionFillTransparency = 0.9,
|
|
||||||
Enabled = {
|
|
||||||
TextColor = hexColor(0x393939),
|
|
||||||
BorderColor = hexColor(0xACACAC),
|
|
||||||
},
|
|
||||||
Disabled = {
|
|
||||||
TextColor = hexColor(0x393939),
|
|
||||||
BorderColor = hexColor(0xACACAC),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Checkbox = {
|
|
||||||
Active = {
|
|
||||||
IconColor = hexColor(0xFFFFFF),
|
|
||||||
BackgroundColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Inactive = {
|
|
||||||
IconColor = hexColor(0xEEEEEE),
|
|
||||||
BorderColor = hexColor(0xAFAFAF),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Dropdown = {
|
|
||||||
TextColor = hexColor(0x00000),
|
|
||||||
BorderColor = hexColor(0xAFAFAF),
|
|
||||||
BackgroundColor = hexColor(0xEEEEEE),
|
|
||||||
Open = {
|
|
||||||
IconColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Closed = {
|
|
||||||
IconColor = hexColor(0xEEEEEE),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
AddressEntry = {
|
|
||||||
TextColor = hexColor(0x000000),
|
|
||||||
PlaceholderColor = hexColor(0x8C8C8C)
|
|
||||||
},
|
|
||||||
BorderedContainer = {
|
|
||||||
BorderColor = hexColor(0xCBCBCB),
|
|
||||||
BackgroundColor = hexColor(0xEEEEEE),
|
|
||||||
},
|
|
||||||
Spinner = {
|
|
||||||
ForegroundColor = BRAND_COLOR,
|
|
||||||
BackgroundColor = hexColor(0xEEEEEE),
|
|
||||||
},
|
|
||||||
Diff = {
|
|
||||||
Add = hexColor(0xbaffbd),
|
|
||||||
Remove = hexColor(0xffbdba),
|
|
||||||
Edit = hexColor(0xbacdff),
|
|
||||||
Row = hexColor(0x000000),
|
|
||||||
},
|
|
||||||
ConnectionDetails = {
|
|
||||||
ProjectNameColor = hexColor(0x00000),
|
|
||||||
AddressColor = hexColor(0x00000),
|
|
||||||
DisconnectColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Settings = {
|
|
||||||
DividerColor = hexColor(0xCBCBCB),
|
|
||||||
Navbar = {
|
|
||||||
BackButtonColor = hexColor(0x000000),
|
|
||||||
TextColor = hexColor(0x000000),
|
|
||||||
},
|
|
||||||
Setting = {
|
|
||||||
NameColor = hexColor(0x000000),
|
|
||||||
DescriptionColor = hexColor(0x5F5F5F),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Header = {
|
|
||||||
LogoColor = BRAND_COLOR,
|
|
||||||
VersionColor = hexColor(0x727272),
|
|
||||||
},
|
|
||||||
Notification = {
|
|
||||||
InfoColor = hexColor(0x00000),
|
|
||||||
CloseColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
ErrorColor = hexColor(0x000000),
|
|
||||||
ScrollBarColor = hexColor(0x000000),
|
|
||||||
})
|
|
||||||
|
|
||||||
local darkTheme = strict("DarkTheme", {
|
|
||||||
BackgroundColor = hexColor(0x2E2E2E),
|
|
||||||
Button = {
|
|
||||||
Solid = {
|
|
||||||
ActionFillColor = hexColor(0xFFFFFF),
|
|
||||||
ActionFillTransparency = 0.8,
|
|
||||||
Enabled = {
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
BackgroundColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Disabled = {
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
BackgroundColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Bordered = {
|
|
||||||
ActionFillColor = hexColor(0xFFFFFF),
|
|
||||||
ActionFillTransparency = 0.9,
|
|
||||||
Enabled = {
|
|
||||||
TextColor = hexColor(0xDBDBDB),
|
|
||||||
BorderColor = hexColor(0x535353),
|
|
||||||
},
|
|
||||||
Disabled = {
|
|
||||||
TextColor = hexColor(0xDBDBDB),
|
|
||||||
BorderColor = hexColor(0x535353),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Checkbox = {
|
|
||||||
Active = {
|
|
||||||
IconColor = hexColor(0xFFFFFF),
|
|
||||||
BackgroundColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Inactive = {
|
|
||||||
IconColor = hexColor(0x484848),
|
|
||||||
BorderColor = hexColor(0x5A5A5A),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Dropdown = {
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
BorderColor = hexColor(0x5A5A5A),
|
|
||||||
BackgroundColor = hexColor(0x2B2B2B),
|
|
||||||
Open = {
|
|
||||||
IconColor = BRAND_COLOR,
|
|
||||||
},
|
|
||||||
Closed = {
|
|
||||||
IconColor = hexColor(0x484848),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
AddressEntry = {
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
PlaceholderColor = hexColor(0x8B8B8B)
|
|
||||||
},
|
|
||||||
BorderedContainer = {
|
|
||||||
BorderColor = hexColor(0x535353),
|
|
||||||
BackgroundColor = hexColor(0x2B2B2B),
|
|
||||||
},
|
|
||||||
Spinner = {
|
|
||||||
ForegroundColor = BRAND_COLOR,
|
|
||||||
BackgroundColor = hexColor(0x2B2B2B),
|
|
||||||
},
|
|
||||||
Diff = {
|
|
||||||
Add = hexColor(0x273732),
|
|
||||||
Remove = hexColor(0x3F2D32),
|
|
||||||
Edit = hexColor(0x193345),
|
|
||||||
Row = hexColor(0xFFFFFF),
|
|
||||||
},
|
|
||||||
ConnectionDetails = {
|
|
||||||
ProjectNameColor = hexColor(0xFFFFFF),
|
|
||||||
AddressColor = hexColor(0xFFFFFF),
|
|
||||||
DisconnectColor = hexColor(0xFFFFFF),
|
|
||||||
},
|
|
||||||
Settings = {
|
|
||||||
DividerColor = hexColor(0x535353),
|
|
||||||
Navbar = {
|
|
||||||
BackButtonColor = hexColor(0xFFFFFF),
|
|
||||||
TextColor = hexColor(0xFFFFFF),
|
|
||||||
},
|
|
||||||
Setting = {
|
|
||||||
NameColor = hexColor(0xFFFFFF),
|
|
||||||
DescriptionColor = hexColor(0xD3D3D3),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Header = {
|
|
||||||
LogoColor = BRAND_COLOR,
|
|
||||||
VersionColor = hexColor(0xD3D3D3)
|
|
||||||
},
|
|
||||||
Notification = {
|
|
||||||
InfoColor = hexColor(0xFFFFFF),
|
|
||||||
CloseColor = hexColor(0xFFFFFF),
|
|
||||||
},
|
|
||||||
ErrorColor = hexColor(0xFFFFFF),
|
|
||||||
ScrollBarColor = hexColor(0xFFFFFF),
|
|
||||||
})
|
|
||||||
|
|
||||||
local Context = Roact.createContext(lightTheme)
|
|
||||||
|
|
||||||
local StudioProvider = Roact.Component:extend("StudioProvider")
|
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||||
|
|
||||||
@@ -232,22 +32,161 @@ local StudioProvider = Roact.Component:extend("StudioProvider")
|
|||||||
function StudioProvider:updateTheme()
|
function StudioProvider:updateTheme()
|
||||||
local studioTheme = getStudio().Theme
|
local studioTheme = getStudio().Theme
|
||||||
|
|
||||||
if studioTheme.Name == "Light" then
|
local isDark = studioTheme.Name == "Dark"
|
||||||
self:setState({
|
|
||||||
theme = lightTheme,
|
local theme = strict(studioTheme.Name .. "Theme", {
|
||||||
|
BrandColor = BRAND_COLOR,
|
||||||
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
|
SubTextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
|
||||||
|
Button = {
|
||||||
|
Solid = {
|
||||||
|
-- Solid uses brand theming, not Studio theming.
|
||||||
|
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||||
|
ActionFillTransparency = 0.8,
|
||||||
|
Enabled = {
|
||||||
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
|
BackgroundColor = BRAND_COLOR,
|
||||||
|
},
|
||||||
|
Disabled = {
|
||||||
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
|
BackgroundColor = BRAND_COLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bordered = {
|
||||||
|
ActionFillColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.ButtonText,
|
||||||
|
Enum.StudioStyleGuideModifier.Selected
|
||||||
|
),
|
||||||
|
ActionFillTransparency = 0.9,
|
||||||
|
Enabled = {
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Disabled = {
|
||||||
|
TextColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.ButtonText,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Checkbox = {
|
||||||
|
Active = {
|
||||||
|
-- Active checkboxes use brand theming, not Studio theming.
|
||||||
|
IconColor = Color3.fromHex("FFFFFF"),
|
||||||
|
BackgroundColor = BRAND_COLOR,
|
||||||
|
},
|
||||||
|
Inactive = {
|
||||||
|
IconColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldIndicator,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dropdown = {
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
|
||||||
|
IconColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldIndicator,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
},
|
||||||
|
TextInput = {
|
||||||
|
Enabled = {
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Disabled = {
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
|
PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ActionFillColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
ActionFillTransparency = 0.9,
|
||||||
|
},
|
||||||
|
AddressEntry = {
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
|
||||||
|
},
|
||||||
|
BorderedContainer = {
|
||||||
|
BorderColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
||||||
|
},
|
||||||
|
Spinner = {
|
||||||
|
ForegroundColor = BRAND_COLOR,
|
||||||
|
BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
|
||||||
|
},
|
||||||
|
Diff = {
|
||||||
|
-- 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),
|
||||||
|
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
||||||
|
},
|
||||||
|
ConnectionDetails = {
|
||||||
|
ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
AddressColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
DisconnectColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
},
|
||||||
|
Settings = {
|
||||||
|
DividerColor = studioTheme:GetColor(
|
||||||
|
Enum.StudioStyleGuideColor.CheckedFieldBorder,
|
||||||
|
Enum.StudioStyleGuideModifier.Disabled
|
||||||
|
),
|
||||||
|
Navbar = {
|
||||||
|
BackButtonColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
},
|
||||||
|
Setting = {
|
||||||
|
NameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
DescriptionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
|
UnstableColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
||||||
|
DebugColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InfoText),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Header = {
|
||||||
|
LogoColor = BRAND_COLOR,
|
||||||
|
VersionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||||
|
},
|
||||||
|
Notification = {
|
||||||
|
InfoColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
CloseColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
},
|
||||||
|
ErrorColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
|
ScrollBarColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||||
})
|
})
|
||||||
elseif studioTheme.Name == "Dark" then
|
|
||||||
self:setState({
|
|
||||||
theme = darkTheme,
|
|
||||||
})
|
|
||||||
else
|
|
||||||
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
|
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
theme = lightTheme,
|
theme = theme,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
function StudioProvider:init()
|
function StudioProvider:init()
|
||||||
self:updateTheme()
|
self:updateTheme()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||||
local Players = game:GetService("Players")
|
local Players = game:GetService("Players")
|
||||||
local ServerStorage = game:GetService("ServerStorage")
|
local ServerStorage = game:GetService("ServerStorage")
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
@@ -17,8 +19,11 @@ local Dictionary = require(Plugin.Dictionary)
|
|||||||
local ServeSession = require(Plugin.ServeSession)
|
local ServeSession = require(Plugin.ServeSession)
|
||||||
local ApiContext = require(Plugin.ApiContext)
|
local ApiContext = require(Plugin.ApiContext)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
|
local PatchTree = require(Plugin.PatchTree)
|
||||||
local preloadAssets = require(Plugin.preloadAssets)
|
local preloadAssets = require(Plugin.preloadAssets)
|
||||||
local soundPlayer = require(Plugin.soundPlayer)
|
local soundPlayer = require(Plugin.soundPlayer)
|
||||||
|
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
||||||
|
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)
|
||||||
@@ -51,58 +56,262 @@ function App:init()
|
|||||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
||||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
||||||
|
|
||||||
self.patchInfo, self.setPatchInfo = Roact.createBinding({
|
|
||||||
patch = PatchSet.newEmpty(),
|
|
||||||
timestamp = os.time(),
|
|
||||||
})
|
|
||||||
self.confirmationBindable = Instance.new("BindableEvent")
|
self.confirmationBindable = Instance.new("BindableEvent")
|
||||||
self.confirmationEvent = self.confirmationBindable.Event
|
self.confirmationEvent = self.confirmationBindable.Event
|
||||||
|
self.knownProjects = {}
|
||||||
|
self.notifId = 0
|
||||||
|
|
||||||
|
self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string)
|
||||||
|
if not string.find(action, "^Rojo: Patch") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local undoConnection, redoConnection = nil, nil
|
||||||
|
local function cleanup()
|
||||||
|
undoConnection:Disconnect()
|
||||||
|
redoConnection:Disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.warn(
|
||||||
|
string.format(
|
||||||
|
"You've undone '%s'.\nIf this was not intended, please Redo in the topbar or with Ctrl/⌘+Y.",
|
||||||
|
action
|
||||||
|
)
|
||||||
|
)
|
||||||
|
local dismissNotif = self:addNotification(
|
||||||
|
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||||
|
10,
|
||||||
|
{
|
||||||
|
Restore = {
|
||||||
|
text = "Restore",
|
||||||
|
style = "Solid",
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = function(notification)
|
||||||
|
cleanup()
|
||||||
|
notification:dismiss()
|
||||||
|
ChangeHistoryService:Redo()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
Dismiss = {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = function(notification)
|
||||||
|
cleanup()
|
||||||
|
notification:dismiss()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
||||||
|
-- Our notif is now out of date- redoing will not restore the patch
|
||||||
|
-- since we've undone even further. Dismiss the notif.
|
||||||
|
cleanup()
|
||||||
|
dismissNotif()
|
||||||
|
end)
|
||||||
|
redoConnection = ChangeHistoryService.OnRedo:Once(function(redoneAction: string)
|
||||||
|
if redoneAction == action then
|
||||||
|
-- The user has restored the patch, so we can dismiss the notif
|
||||||
|
cleanup()
|
||||||
|
dismissNotif()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
self.disconnectUpdatesCheckChanged = Settings:onChanged("checkForUpdates", function()
|
||||||
|
self:checkForUpdates()
|
||||||
|
end)
|
||||||
|
self.disconnectPrereleasesCheckChanged = Settings:onChanged("checkForPrereleases", function()
|
||||||
|
self:checkForUpdates()
|
||||||
|
end)
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
guiEnabled = false,
|
guiEnabled = false,
|
||||||
confirmData = {},
|
confirmData = {},
|
||||||
|
patchData = {
|
||||||
|
patch = PatchSet.newEmpty(),
|
||||||
|
unapplied = PatchSet.newEmpty(),
|
||||||
|
timestamp = os.time(),
|
||||||
|
},
|
||||||
notifications = {},
|
notifications = {},
|
||||||
toolbarIcon = Assets.Images.PluginButton,
|
toolbarIcon = Assets.Images.PluginButton,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if RunService:IsEdit() then
|
||||||
|
self:checkForUpdates()
|
||||||
|
|
||||||
|
if
|
||||||
|
Settings:get("syncReminder")
|
||||||
|
and self.serveSession == nil
|
||||||
|
and self:getLastSyncTimestamp()
|
||||||
|
and (self:isSyncLockAvailable())
|
||||||
|
then
|
||||||
|
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
|
||||||
|
Connect = {
|
||||||
|
text = "Connect",
|
||||||
|
style = "Solid",
|
||||||
|
layoutOrder = 1,
|
||||||
|
onClick = function(notification)
|
||||||
|
notification:dismiss()
|
||||||
|
self:startSession()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
Dismiss = {
|
||||||
|
text = "Dismiss",
|
||||||
|
style = "Bordered",
|
||||||
|
layoutOrder = 2,
|
||||||
|
onClick = function(notification)
|
||||||
|
notification:dismiss()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:addNotification(text: string, timeout: number?)
|
if self:isAutoConnectPlaytestServerAvailable() then
|
||||||
|
self:useRunningConnectionInfo()
|
||||||
|
self:startSession()
|
||||||
|
end
|
||||||
|
self.autoConnectPlaytestServerListener = Settings:onChanged("autoConnectPlaytestServer", function(enabled)
|
||||||
|
if enabled then
|
||||||
|
if self:isAutoConnectPlaytestServerWriteable() and self.serveSession ~= nil then
|
||||||
|
-- Write the existing session
|
||||||
|
local baseUrl = self.serveSession.__apiContext.__baseUrl
|
||||||
|
self:setRunningConnectionInfo(baseUrl)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:clearRunningConnectionInfo()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:willUnmount()
|
||||||
|
self.waypointConnection:Disconnect()
|
||||||
|
self.confirmationBindable:Destroy()
|
||||||
|
|
||||||
|
self.disconnectUpdatesCheckChanged()
|
||||||
|
self.disconnectPrereleasesCheckChanged()
|
||||||
|
|
||||||
|
self.autoConnectPlaytestServerListener()
|
||||||
|
self:clearRunningConnectionInfo()
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:addNotification(
|
||||||
|
text: string,
|
||||||
|
timeout: number?,
|
||||||
|
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
|
||||||
|
)
|
||||||
if not Settings:get("showNotifications") then
|
if not Settings:get("showNotifications") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self.notifId += 1
|
||||||
|
local id = self.notifId
|
||||||
|
|
||||||
local notifications = table.clone(self.state.notifications)
|
local notifications = table.clone(self.state.notifications)
|
||||||
table.insert(notifications, {
|
notifications[id] = {
|
||||||
text = text,
|
text = text,
|
||||||
timestamp = DateTime.now().UnixTimestampMillis,
|
timestamp = DateTime.now().UnixTimestampMillis,
|
||||||
timeout = timeout or 3,
|
timeout = timeout or 3,
|
||||||
|
actions = actions,
|
||||||
|
}
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
notifications = notifications,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return function()
|
||||||
|
self:closeNotification(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:closeNotification(id: number)
|
||||||
|
if not self.state.notifications[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local notifications = table.clone(self.state.notifications)
|
||||||
|
notifications[id] = nil
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
notifications = notifications,
|
notifications = notifications,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:closeNotification(index: number)
|
function App:checkForUpdates()
|
||||||
local notifications = table.clone(self.state.notifications)
|
if not Settings:get("checkForUpdates") then
|
||||||
table.remove(notifications, index)
|
return
|
||||||
|
end
|
||||||
|
|
||||||
self:setState({
|
local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil
|
||||||
notifications = notifications,
|
local latestCompatibleVersion = Version.retrieveLatestCompatible({
|
||||||
|
version = Config.version,
|
||||||
|
includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"),
|
||||||
})
|
})
|
||||||
|
if not latestCompatibleVersion then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
function App:getPriorEndpoint()
|
function App:getPriorEndpoint()
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then return end
|
if not priorEndpoints then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local place = priorEndpoints[tostring(game.PlaceId)]
|
local id = tostring(game.PlaceId)
|
||||||
if not place then return end
|
if ignorePlaceIds[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local place = priorEndpoints[id]
|
||||||
|
if not place then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
return place.host, place.port
|
return place.host, place.port
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function App:getLastSyncTimestamp()
|
||||||
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
|
if not priorEndpoints then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = tostring(game.PlaceId)
|
||||||
|
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)
|
function App:setPriorEndpoint(host: string, port: string)
|
||||||
local priorEndpoints = Settings:get("priorEndpoints")
|
local priorEndpoints = Settings:get("priorEndpoints")
|
||||||
if not priorEndpoints then
|
if not priorEndpoints then
|
||||||
@@ -117,17 +326,17 @@ function App:setPriorEndpoint(host: string, port: string)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if host == Config.defaultHost and port == Config.defaultPort then
|
local id = tostring(game.PlaceId)
|
||||||
-- Don't save default
|
if ignorePlaceIds[id] then
|
||||||
priorEndpoints[tostring(game.PlaceId)] = nil
|
return
|
||||||
else
|
end
|
||||||
priorEndpoints[tostring(game.PlaceId)] = {
|
|
||||||
host = host ~= Config.defaultHost and host or nil,
|
priorEndpoints[id] = {
|
||||||
port = port ~= Config.defaultPort and port or nil,
|
host = if host ~= Config.defaultHost then host else nil,
|
||||||
|
port = if port ~= Config.defaultPort then port else nil,
|
||||||
timestamp = os.time(),
|
timestamp = os.time(),
|
||||||
}
|
}
|
||||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||||
end
|
|
||||||
|
|
||||||
Settings:set("priorEndpoints", priorEndpoints)
|
Settings:set("priorEndpoints", priorEndpoints)
|
||||||
end
|
end
|
||||||
@@ -136,10 +345,28 @@ function App:getHostAndPort()
|
|||||||
local host = self.host:getValue()
|
local host = self.host:getValue()
|
||||||
local port = self.port:getValue()
|
local port = self.port:getValue()
|
||||||
|
|
||||||
local host = if #host > 0 then host else Config.defaultHost
|
return if #host > 0 then host else Config.defaultHost, if #port > 0 then port else Config.defaultPort
|
||||||
local port = if #port > 0 then port else Config.defaultPort
|
end
|
||||||
|
|
||||||
return host, port
|
function App:isSyncLockAvailable()
|
||||||
|
if #Players:GetPlayers() == 0 then
|
||||||
|
-- Team Create is not active, so no one can be holding the lock
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
||||||
|
if not lock then
|
||||||
|
-- No lock is made yet, so it is available
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if lock.Value and lock.Value ~= Players.LocalPlayer and lock.Value.Parent then
|
||||||
|
-- Someone else is holding the lock
|
||||||
|
return false, lock.Value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The lock exists, but is not claimed
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:claimSyncLock()
|
function App:claimSyncLock()
|
||||||
@@ -148,6 +375,12 @@ function App:claimSyncLock()
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local isAvailable, priorOwner = self:isSyncLockAvailable()
|
||||||
|
if not isAvailable then
|
||||||
|
Log.trace("Skipping sync lock because it is already claimed")
|
||||||
|
return false, priorOwner
|
||||||
|
end
|
||||||
|
|
||||||
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
||||||
if not lock then
|
if not lock then
|
||||||
lock = Instance.new("ObjectValue")
|
lock = Instance.new("ObjectValue")
|
||||||
@@ -159,11 +392,6 @@ function App:claimSyncLock()
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
if lock.Value and lock.Value ~= Players.LocalPlayer and lock.Value.Parent then
|
|
||||||
Log.trace("Found existing sync lock owned by {}", lock.Value)
|
|
||||||
return false, lock.Value
|
|
||||||
end
|
|
||||||
|
|
||||||
lock.Value = Players.LocalPlayer
|
lock.Value = Players.LocalPlayer
|
||||||
Log.trace("Claimed existing sync lock")
|
Log.trace("Claimed existing sync lock")
|
||||||
return true
|
return true
|
||||||
@@ -185,6 +413,49 @@ 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:isAutoConnectPlaytestServerAvailable()
|
||||||
|
return RunService:IsRunMode()
|
||||||
|
and RunService:IsServer()
|
||||||
|
and Settings:get("autoConnectPlaytestServer")
|
||||||
|
and workspace:GetAttribute("__Rojo_ConnectionUrl")
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:isAutoConnectPlaytestServerWriteable()
|
||||||
|
return RunService:IsEdit() and Settings:get("autoConnectPlaytestServer")
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:setRunningConnectionInfo(baseUrl: string)
|
||||||
|
if not self:isAutoConnectPlaytestServerWriteable() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.trace("Setting connection info for play solo auto-connect")
|
||||||
|
workspace:SetAttribute("__Rojo_ConnectionUrl", baseUrl)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:clearRunningConnectionInfo()
|
||||||
|
if not RunService:IsEdit() then
|
||||||
|
-- Only write connection info from edit mode
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.trace("Clearing connection info for play solo auto-connect")
|
||||||
|
workspace:SetAttribute("__Rojo_ConnectionUrl", nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:useRunningConnectionInfo()
|
||||||
|
local connectionInfo = workspace:GetAttribute("__Rojo_ConnectionUrl")
|
||||||
|
if not connectionInfo then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.trace("Using connection info for play solo auto-connect")
|
||||||
|
local host, port = string.match(connectionInfo, "^(.+):(.-)$")
|
||||||
|
|
||||||
|
self.setHost(host)
|
||||||
|
self.setPort(port)
|
||||||
|
end
|
||||||
|
|
||||||
function App:startSession()
|
function App:startSession()
|
||||||
local claimedLock, priorOwner = self:claimSyncLock()
|
local claimedLock, priorOwner = self:claimSyncLock()
|
||||||
if not claimedLock then
|
if not claimedLock then
|
||||||
@@ -203,11 +474,6 @@ function App:startSession()
|
|||||||
|
|
||||||
local host, port = self:getHostAndPort()
|
local host, port = self:getHostAndPort()
|
||||||
|
|
||||||
local sessionOptions = {
|
|
||||||
openScriptsExternally = Settings:get("openScriptsExternally"),
|
|
||||||
twoWaySync = Settings:get("twoWaySync"),
|
|
||||||
}
|
|
||||||
|
|
||||||
local baseUrl = if string.find(host, "^https?://")
|
local baseUrl = if string.find(host, "^https?://")
|
||||||
then string.format("%s:%s", host, port)
|
then string.format("%s:%s", host, port)
|
||||||
else string.format("http://%s:%s", host, port)
|
else string.format("http://%s:%s", host, port)
|
||||||
@@ -215,35 +481,54 @@ function App:startSession()
|
|||||||
|
|
||||||
local serveSession = ServeSession.new({
|
local serveSession = ServeSession.new({
|
||||||
apiContext = apiContext,
|
apiContext = apiContext,
|
||||||
openScriptsExternally = sessionOptions.openScriptsExternally,
|
twoWaySync = Settings:get("twoWaySync"),
|
||||||
twoWaySync = sessionOptions.twoWaySync,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
serveSession:onPatchApplied(function(patch, _unapplied)
|
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
|
||||||
|
-- Build new tree for patch
|
||||||
|
self:setState({
|
||||||
|
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||||
|
-- Update tree with unapplied metadata
|
||||||
|
self:setState(function(prevState)
|
||||||
|
return {
|
||||||
|
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
|
||||||
|
local now = DateTime.now().UnixTimestamp
|
||||||
|
local old = self.state.patchData
|
||||||
|
|
||||||
if PatchSet.isEmpty(patch) then
|
if PatchSet.isEmpty(patch) then
|
||||||
-- Ignore empty patches
|
-- Ignore empty patch, but update timestamp
|
||||||
|
self:setState({
|
||||||
|
patchData = {
|
||||||
|
patch = old.patch,
|
||||||
|
unapplied = old.unapplied,
|
||||||
|
timestamp = now,
|
||||||
|
},
|
||||||
|
})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local now = os.time()
|
|
||||||
|
|
||||||
local old = self.patchInfo:getValue()
|
|
||||||
if now - old.timestamp < 2 then
|
if now - old.timestamp < 2 then
|
||||||
-- Patches that apply in the same second are
|
-- Patches that apply in the same second are
|
||||||
-- considered to be part of the same change for human clarity
|
-- considered to be part of the same change for human clarity
|
||||||
local merged = PatchSet.newEmpty()
|
patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
|
||||||
PatchSet.assign(merged, old.patch, patch)
|
unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
|
||||||
|
|
||||||
self.setPatchInfo({
|
|
||||||
patch = merged,
|
|
||||||
timestamp = now,
|
|
||||||
})
|
|
||||||
else
|
|
||||||
self.setPatchInfo({
|
|
||||||
patch = patch,
|
|
||||||
timestamp = now,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
patchData = {
|
||||||
|
patch = patch,
|
||||||
|
unapplied = unapplied,
|
||||||
|
timestamp = now,
|
||||||
|
},
|
||||||
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
serveSession:onStatusChanged(function(status, details)
|
serveSession:onStatusChanged(function(status, details)
|
||||||
@@ -256,6 +541,9 @@ function App:startSession()
|
|||||||
})
|
})
|
||||||
self:addNotification("Connecting to session...")
|
self:addNotification("Connecting to session...")
|
||||||
elseif status == ServeSession.Status.Connected then
|
elseif status == ServeSession.Status.Connected then
|
||||||
|
self.knownProjects[details] = true
|
||||||
|
self:setRunningConnectionInfo(baseUrl)
|
||||||
|
|
||||||
local address = ("%s:%s"):format(host, port)
|
local address = ("%s:%s"):format(host, port)
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Connected,
|
appStatus = AppStatus.Connected,
|
||||||
@@ -267,6 +555,14 @@ function App:startSession()
|
|||||||
elseif status == ServeSession.Status.Disconnected then
|
elseif status == ServeSession.Status.Disconnected then
|
||||||
self.serveSession = nil
|
self.serveSession = nil
|
||||||
self:releaseSyncLock()
|
self:releaseSyncLock()
|
||||||
|
self:clearRunningConnectionInfo()
|
||||||
|
self:setState({
|
||||||
|
patchData = {
|
||||||
|
patch = PatchSet.newEmpty(),
|
||||||
|
unapplied = PatchSet.newEmpty(),
|
||||||
|
timestamp = os.time(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
-- Details being present indicates that this
|
-- Details being present indicates that this
|
||||||
-- disconnection was from an error.
|
-- disconnection was from an error.
|
||||||
@@ -291,9 +587,67 @@ function App:startSession()
|
|||||||
|
|
||||||
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
||||||
if PatchSet.isEmpty(patch) then
|
if PatchSet.isEmpty(patch) then
|
||||||
|
Log.trace("Accepting patch without confirmation because it is empty")
|
||||||
return "Accept"
|
return "Accept"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Play solo auto-connect does not require confirmation
|
||||||
|
if self:isAutoConnectPlaytestServerAvailable() then
|
||||||
|
Log.trace("Accepting patch without confirmation because play solo auto-connect is enabled")
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
|
||||||
|
local confirmationBehavior = Settings:get("confirmationBehavior")
|
||||||
|
if confirmationBehavior == "Initial" then
|
||||||
|
-- Only confirm if we haven't synced this project yet this session
|
||||||
|
if self.knownProjects[serverInfo.projectName] then
|
||||||
|
Log.trace(
|
||||||
|
"Accepting patch without confirmation because project has already been connected and behavior is set to Initial"
|
||||||
|
)
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
elseif confirmationBehavior == "Large Changes" then
|
||||||
|
-- Only confirm if the patch impacts many instances
|
||||||
|
if PatchSet.countInstances(patch) < Settings:get("largeChangesConfirmationThreshold") then
|
||||||
|
Log.trace(
|
||||||
|
"Accepting patch without confirmation because patch is small and behavior is set to Large Changes"
|
||||||
|
)
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
elseif confirmationBehavior == "Unlisted PlaceId" then
|
||||||
|
-- Only confirm if the current placeId is not in the servePlaceIds allowlist
|
||||||
|
if serverInfo.expectedPlaceIds then
|
||||||
|
local isListed = table.find(serverInfo.expectedPlaceIds, game.PlaceId) ~= nil
|
||||||
|
if isListed then
|
||||||
|
Log.trace(
|
||||||
|
"Accepting patch without confirmation because placeId is listed and behavior is set to Unlisted PlaceId"
|
||||||
|
)
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif confirmationBehavior == "Never" then
|
||||||
|
Log.trace("Accepting patch without confirmation because behavior is set to Never")
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The datamodel name gets overwritten by Studio, making confirmation of it intrusive
|
||||||
|
-- and unnecessary. This special case allows it to be accepted without confirmation.
|
||||||
|
if
|
||||||
|
PatchSet.hasAdditions(patch) == false
|
||||||
|
and PatchSet.hasRemoves(patch) == false
|
||||||
|
and PatchSet.containsOnlyInstance(patch, instanceMap, game)
|
||||||
|
then
|
||||||
|
local datamodelUpdates = PatchSet.getUpdateForInstance(patch, instanceMap, game)
|
||||||
|
if
|
||||||
|
datamodelUpdates ~= nil
|
||||||
|
and next(datamodelUpdates.changedProperties) == nil
|
||||||
|
and datamodelUpdates.changedClassName == nil
|
||||||
|
then
|
||||||
|
Log.trace("Accepting patch without confirmation because it only contains a datamodel name change")
|
||||||
|
return "Accept"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Confirming,
|
appStatus = AppStatus.Confirming,
|
||||||
confirmData = {
|
confirmData = {
|
||||||
@@ -318,16 +672,6 @@ function App:startSession()
|
|||||||
serveSession:start()
|
serveSession:start()
|
||||||
|
|
||||||
self.serveSession = serveSession
|
self.serveSession = serveSession
|
||||||
|
|
||||||
task.defer(function()
|
|
||||||
while self.serveSession == serveSession do
|
|
||||||
-- Trigger rerender to update timestamp text
|
|
||||||
local patchInfo = table.clone(self.patchInfo:getValue())
|
|
||||||
self.setPatchInfo(patchInfo)
|
|
||||||
local elapsed = os.time() - patchInfo.timestamp
|
|
||||||
task.wait(elapsed < 60 and 1 or elapsed / 5)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:endSession()
|
function App:endSession()
|
||||||
@@ -343,6 +687,13 @@ function App:endSession()
|
|||||||
appStatus = AppStatus.NotConnected,
|
appStatus = AppStatus.NotConnected,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.cleanupPrecommit ~= nil then
|
||||||
|
self.cleanupPrecommit()
|
||||||
|
end
|
||||||
|
if self.cleanupPostcommit ~= nil then
|
||||||
|
self.cleanupPostcommit()
|
||||||
|
end
|
||||||
|
|
||||||
Log.trace("Session terminated by user")
|
Log.trace("Session terminated by user")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -364,17 +715,17 @@ function App:render()
|
|||||||
value = self.props.plugin,
|
value = self.props.plugin,
|
||||||
}, {
|
}, {
|
||||||
e(Theme.StudioProvider, nil, {
|
e(Theme.StudioProvider, nil, {
|
||||||
e(Tooltip.Provider, nil, {
|
tooltip = e(Tooltip.Provider, nil, {
|
||||||
gui = e(StudioPluginGui, {
|
gui = e(StudioPluginGui, {
|
||||||
id = pluginName,
|
id = pluginName,
|
||||||
title = pluginName,
|
title = pluginName,
|
||||||
active = self.state.guiEnabled,
|
active = self.state.guiEnabled,
|
||||||
|
isEphemeral = false,
|
||||||
|
|
||||||
initDockState = Enum.InitialDockState.Right,
|
initDockState = Enum.InitialDockState.Right,
|
||||||
initEnabled = false,
|
|
||||||
overridePreviousState = false,
|
overridePreviousState = false,
|
||||||
floatingSize = Vector2.new(300, 200),
|
floatingSize = Vector2.new(320, 210),
|
||||||
minimumSize = Vector2.new(300, 120),
|
minimumSize = Vector2.new(300, 210),
|
||||||
|
|
||||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
@@ -403,6 +754,7 @@ function App:render()
|
|||||||
end,
|
end,
|
||||||
|
|
||||||
onNavigateSettings = function()
|
onNavigateSettings = function()
|
||||||
|
self.backPage = AppStatus.NotConnected
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.Settings,
|
appStatus = AppStatus.Settings,
|
||||||
})
|
})
|
||||||
@@ -429,18 +781,29 @@ function App:render()
|
|||||||
Connected = createPageElement(AppStatus.Connected, {
|
Connected = createPageElement(AppStatus.Connected, {
|
||||||
projectName = self.state.projectName,
|
projectName = self.state.projectName,
|
||||||
address = self.state.address,
|
address = self.state.address,
|
||||||
patchInfo = self.patchInfo,
|
patchTree = self.state.patchTree,
|
||||||
|
patchData = self.state.patchData,
|
||||||
serveSession = self.serveSession,
|
serveSession = self.serveSession,
|
||||||
|
|
||||||
onDisconnect = function()
|
onDisconnect = function()
|
||||||
self:endSession()
|
self:endSession()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
onNavigateSettings = function()
|
||||||
|
self.backPage = AppStatus.Connected
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Settings,
|
||||||
|
})
|
||||||
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Settings = createPageElement(AppStatus.Settings, {
|
Settings = createPageElement(AppStatus.Settings, {
|
||||||
|
syncActive = self.serveSession ~= nil
|
||||||
|
and self.serveSession:getStatus() == ServeSession.Status.Connected,
|
||||||
|
|
||||||
onBack = function()
|
onBack = function()
|
||||||
self:setState({
|
self:setState({
|
||||||
appStatus = AppStatus.NotConnected,
|
appStatus = self.backPage or AppStatus.NotConnected,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
@@ -457,7 +820,11 @@ function App:render()
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
RojoNotifications = e("ScreenGui", {}, {
|
RojoNotifications = e("ScreenGui", {
|
||||||
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
ResetOnSpawn = false,
|
||||||
|
DisplayOrder = 100,
|
||||||
|
}, {
|
||||||
layout = e("UIListLayout", {
|
layout = e("UIListLayout", {
|
||||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
@@ -473,8 +840,8 @@ function App:render()
|
|||||||
notifs = e(Notifications, {
|
notifs = e(Notifications, {
|
||||||
soundPlayer = self.props.soundPlayer,
|
soundPlayer = self.props.soundPlayer,
|
||||||
notifications = self.state.notifications,
|
notifications = self.state.notifications,
|
||||||
onClose = function(index)
|
onClose = function(id)
|
||||||
self:closeNotification(index)
|
self:closeNotification(id)
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ local Assets = {
|
|||||||
Close = "rbxassetid://6012985953",
|
Close = "rbxassetid://6012985953",
|
||||||
Back = "rbxassetid://6017213752",
|
Back = "rbxassetid://6017213752",
|
||||||
Reset = "rbxassetid://10142422327",
|
Reset = "rbxassetid://10142422327",
|
||||||
|
Expand = "rbxassetid://12045401097",
|
||||||
|
Warning = "rbxassetid://16571019891",
|
||||||
|
Debug = "rbxassetid://16588411361",
|
||||||
|
Checkmark = "rbxassetid://16571012729",
|
||||||
|
Exclamation = "rbxassetid://16571172190",
|
||||||
|
SyncSuccess = "rbxassetid://16565035221",
|
||||||
|
SyncWarning = "rbxassetid://16565325171",
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
Add = "rbxassetid://10434145835",
|
Add = "rbxassetid://10434145835",
|
||||||
@@ -33,9 +40,11 @@ local Assets = {
|
|||||||
Checkbox = {
|
Checkbox = {
|
||||||
Active = "rbxassetid://6016251644",
|
Active = "rbxassetid://6016251644",
|
||||||
Inactive = "rbxassetid://6016251963",
|
Inactive = "rbxassetid://6016251963",
|
||||||
|
Locked = "rbxassetid://14011257320",
|
||||||
},
|
},
|
||||||
Dropdown = {
|
Dropdown = {
|
||||||
Arrow = "rbxassetid://10131770538",
|
Arrow = "rbxassetid://10131770538",
|
||||||
|
Locked = "rbxassetid://14011257320",
|
||||||
},
|
},
|
||||||
Spinner = {
|
Spinner = {
|
||||||
Foreground = "rbxassetid://3222731032",
|
Foreground = "rbxassetid://3222731032",
|
||||||
@@ -51,7 +60,7 @@ local Assets = {
|
|||||||
[32] = "rbxassetid://3088713341",
|
[32] = "rbxassetid://3088713341",
|
||||||
[64] = "rbxassetid://4918677124",
|
[64] = "rbxassetid://4918677124",
|
||||||
[128] = "rbxassetid://2600845734",
|
[128] = "rbxassetid://2600845734",
|
||||||
[500] = "rbxassetid://2609138523"
|
[500] = "rbxassetid://2609138523",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Sounds = {
|
Sounds = {
|
||||||
|
|||||||