Compare commits
19 Commits
plugin-tes
...
v7.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bd3c74db0 | ||
|
|
f4e2f5aefc | ||
|
|
8ceb40a24e | ||
|
|
3e53d67412 | ||
|
|
844f51d916 | ||
|
|
26974ffd4c | ||
|
|
91f5b4a675 | ||
|
|
d179240139 | ||
|
|
67b6a7e198 | ||
|
|
3b721242c1 | ||
|
|
c6ceaa5c87 | ||
|
|
af9629c53f | ||
|
|
9509909f46 | ||
|
|
88efbd433f | ||
|
|
f716928683 | ||
|
|
e23d024ba3 | ||
|
|
591419611e | ||
|
|
f68beab1df | ||
|
|
2798610afd |
@@ -1,2 +0,0 @@
|
||||
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
|
||||
(eglot-luau-rojo-sourcemap-enabled . 't))))
|
||||
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.lua linguist-language=Luau
|
||||
6
.github/workflows/changelog.yml
vendored
@@ -11,12 +11,12 @@ jobs:
|
||||
name: Check Actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Changelog check
|
||||
uses: Zomzog/changelog-checker@v1.3.0
|
||||
with:
|
||||
fileName: CHANGELOG.md
|
||||
fileName: CHANGELOG.md
|
||||
noChangelogLabel: skip changelog
|
||||
checkNotification: Simple
|
||||
env:
|
||||
|
||||
117
.github/workflows/ci.yml
vendored
@@ -16,24 +16,27 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- 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: Build
|
||||
run: cargo build --locked --verbose
|
||||
@@ -41,96 +44,56 @@ jobs:
|
||||
- name: Test
|
||||
run: cargo test --locked --verbose
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
msrv:
|
||||
name: Check MSRV
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.79.0
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: 1.70.0
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- 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: Build
|
||||
run: cargo build --locked --verbose
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
test-plugin:
|
||||
name: Test Plugin
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
|
||||
- name: Test
|
||||
run: lune run test-plugin
|
||||
env:
|
||||
RBX_API_KEY: ${{ secrets.PLUGIN_TEST_API_KEY }}
|
||||
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_TEST_UNIVERSE_ID }}
|
||||
RBX_PLACE_ID: ${{ vars.PLUGIN_TEST_PLACE_ID }}
|
||||
|
||||
lint:
|
||||
name: Rustfmt, Clippy, Stylua, & Selene
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.3.0
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
version: 'v0.2.7'
|
||||
|
||||
- name: Stylua
|
||||
run: stylua --check plugin/src
|
||||
@@ -144,11 +107,3 @@ jobs:
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
128
.github/workflows/release.yml
vendored
@@ -8,39 +8,51 @@ jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
build-plugin:
|
||||
needs: ["create-release"]
|
||||
name: Build Roblox Studio Plugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Setup Rokit
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.1.0
|
||||
with:
|
||||
version: 'v1.1.0'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trust-check: false
|
||||
version: 'v0.3.0'
|
||||
|
||||
- name: Build Plugin
|
||||
run: rojo build plugin.project.json --output Rojo.rbxm
|
||||
run: rojo build plugin --output Rojo.rbxm
|
||||
|
||||
- name: Upload Plugin to Release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} Rojo.rbxm
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: Rojo.rbxm
|
||||
asset_name: Rojo.rbxm
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Plugin to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Rojo.rbxm
|
||||
path: Rojo.rbxm
|
||||
@@ -53,25 +65,15 @@ jobs:
|
||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||
include:
|
||||
- host: linux
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-20.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
label: linux-x86_64
|
||||
|
||||
- host: linux
|
||||
os: ubuntu-22.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
label: linux-aarch64
|
||||
|
||||
- host: windows
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
label: windows-x86_64
|
||||
|
||||
- host: windows
|
||||
os: windows-11-arm
|
||||
target: aarch64-pc-windows-msvc
|
||||
label: windows-aarch64
|
||||
|
||||
- host: macos
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
@@ -87,64 +89,70 @@ jobs:
|
||||
env:
|
||||
BIN: rojo
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Get Version from Tag
|
||||
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: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Setup Aftman
|
||||
uses: ok-nick/setup-aftman@v0.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trust-check: false
|
||||
version: 'v0.2.6'
|
||||
|
||||
- name: Build Release
|
||||
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||
|
||||
- name: Save Rust Cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Generate Artifact Name
|
||||
shell: bash
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
|
||||
# Build into a known directory so we can find our build artifact more
|
||||
# easily.
|
||||
CARGO_TARGET_DIR: output
|
||||
|
||||
- name: Create Archive and Upload to Release
|
||||
# On platforms that use OpenSSL, ensure it is statically linked to
|
||||
# make binaries more portable.
|
||||
OPENSSL_STATIC: 1
|
||||
|
||||
- name: Create Release Archive
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
mkdir staging
|
||||
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
cp "target/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||
cd staging
|
||||
7z a ../$ARTIFACT_NAME *
|
||||
7z a ../release.zip *
|
||||
else
|
||||
cp "target/${{ matrix.target }}/release/$BIN" staging/
|
||||
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
||||
cd staging
|
||||
zip ../$ARTIFACT_NAME *
|
||||
zip ../release.zip *
|
||||
fi
|
||||
|
||||
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
|
||||
- name: Upload Archive to Release
|
||||
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
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: ${{ env.ARTIFACT_NAME }}
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
||||
path: release.zip
|
||||
8
.gitignore
vendored
@@ -10,8 +10,8 @@
|
||||
/*.rbxl
|
||||
/*.rbxlx
|
||||
|
||||
# Sourcemap for the Rojo plugin (for better intellisense)
|
||||
/sourcemap.json
|
||||
# Test places for the Roblox Studio Plugin
|
||||
/plugin/*.rbxlx
|
||||
|
||||
# Roblox Studio holds 'lock' files on places
|
||||
*.rbxl.lock
|
||||
@@ -19,7 +19,3 @@
|
||||
|
||||
# Snapshot files from the 'insta' Rust crate
|
||||
**/*.snap.new
|
||||
|
||||
# Macos file system junk
|
||||
._*
|
||||
.DS_STORE
|
||||
|
||||
3
.gitmodules
vendored
@@ -16,6 +16,3 @@
|
||||
[submodule "plugin/Packages/Highlighter"]
|
||||
path = plugin/Packages/Highlighter
|
||||
url = https://github.com/boatbomber/highlighter.git
|
||||
[submodule ".lune/opencloud-execute"]
|
||||
path = .lune/opencloud-execute
|
||||
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"aliases": {
|
||||
"lune": "~/.lune/.typedefs/0.10.2/"
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
local serde = require("@lune/serde")
|
||||
local net = require("@lune/net")
|
||||
local stdio = require("@lune/stdio")
|
||||
local process = require("@lune/process")
|
||||
local fs = require("@lune/fs")
|
||||
|
||||
local luau_execute = require("./opencloud-execute")
|
||||
|
||||
local TEST_SCRIPT = fs.readFile("plugin/run-tests.server.lua")
|
||||
|
||||
local PATH_VERSION_MATCH = "assets/%d+/versions/(.+)"
|
||||
|
||||
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
|
||||
local PLACE_ID = process.env["RBX_PLACE_ID"]
|
||||
local API_KEY = process.env["RBX_API_KEY"]
|
||||
|
||||
if not UNIVERSE_ID then
|
||||
error("no universe ID specified. try providing one with the env var `RBX_UNIVERSE_ID`")
|
||||
end
|
||||
if not PLACE_ID then
|
||||
error("no place ID specified. try providing one with the env var `RBX_PLACE_ID`")
|
||||
end
|
||||
if not API_KEY then
|
||||
error("no API key specified. try providing one with the env var `RBX_API_KEY`")
|
||||
end
|
||||
|
||||
--stylua: ignore
|
||||
local upload_result = process.exec("cargo", {
|
||||
"run", "--",
|
||||
"upload", "plugin/test-place.project.json",
|
||||
"--api_key", API_KEY,
|
||||
"--universe_id", UNIVERSE_ID,
|
||||
"--asset_id", PLACE_ID
|
||||
}, {
|
||||
stdio = "none"
|
||||
})
|
||||
|
||||
if not upload_result.ok then
|
||||
print("Failed to upload plugin test place")
|
||||
print("Not dumping stdout or stderr to avoid leaking secrets")
|
||||
process.exit(1)
|
||||
end
|
||||
|
||||
-- This is /probably/ not necessary because Rojo generally does not have enough
|
||||
-- activity that there will be multiple CI runs happening at once, but
|
||||
-- it's better safe than sorry.
|
||||
local version_response = net.request({
|
||||
method = "GET",
|
||||
url = `https://apis.roblox.com/assets/v1/assets/{PLACE_ID}/versions`,
|
||||
query = {
|
||||
maxPageSize = 1,
|
||||
},
|
||||
headers = {
|
||||
["User-Agent"] = `Rojo/PluginTesting 1.0.0; {_VERSION}`,
|
||||
["x-api-key"] = API_KEY,
|
||||
},
|
||||
})
|
||||
|
||||
if not version_response.ok then
|
||||
error(
|
||||
`Failed to fetch version of Roblox place to run tests on because: {version_response.statusCode} - {version_response.statusMessage}\n{version_response.body}`
|
||||
)
|
||||
end
|
||||
|
||||
local place_version_raw = serde.decode("json", version_response.body).assetVersions[1].path
|
||||
assert(typeof(place_version_raw) == "string", "the result from asset version endpoint was not as expected")
|
||||
|
||||
local place_version = string.match(place_version_raw, PATH_VERSION_MATCH)
|
||||
|
||||
local task = luau_execute.create_task_versioned(UNIVERSE_ID, PLACE_ID, place_version, TEST_SCRIPT)
|
||||
print(`Running test script on {UNIVERSE_ID}/{PLACE_ID}@{place_version}`)
|
||||
print(`Task ID: {luau_execute.task_id(task)}`)
|
||||
|
||||
luau_execute.await_finish(task)
|
||||
print("Output from task:\n")
|
||||
local logs = luau_execute.get_structured_logs(task)
|
||||
for _, log in logs do
|
||||
if log.messageType == "OUTPUT" or log.messageType == "MESSAGE_TYPE_UNSPECIFIED" then
|
||||
stdio.write(stdio.color("reset"))
|
||||
elseif log.messageType == "INFO" then
|
||||
stdio.write(stdio.color("cyan"))
|
||||
elseif log.messageType == "WARNING" then
|
||||
stdio.write(stdio.color("yellow"))
|
||||
elseif log.messageType == "ERROR" then
|
||||
stdio.write(stdio.color("red"))
|
||||
end
|
||||
stdio.write(log.message)
|
||||
stdio.write(`{stdio.color("reset")}\n`)
|
||||
end
|
||||
|
||||
local results = luau_execute.get_output(task)[1]
|
||||
if not results then
|
||||
error("plugin tests did not return any results")
|
||||
end
|
||||
|
||||
local status = luau_execute.check_status(task)
|
||||
if status == "COMPLETE" then
|
||||
if results.failureCount == 0 then
|
||||
process.exit(0)
|
||||
else
|
||||
process.exit(1)
|
||||
end
|
||||
else
|
||||
print()
|
||||
print("Task did not finish successfully")
|
||||
local err = luau_execute.get_error(task)
|
||||
if err then
|
||||
print(`Error from task: {err.code}`)
|
||||
print(err.message)
|
||||
end
|
||||
process.exit(1)
|
||||
end
|
||||
8
.vscode/extensions.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"JohnnyMorganz.luau-lsp",
|
||||
"JohnnyMorganz.stylua",
|
||||
"Kampfkarren.selene-vscode",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
|
||||
"luau-lsp.sourcemap.autogenerate": true
|
||||
}
|
||||
154
CHANGELOG.md
@@ -1,151 +1,6 @@
|
||||
# Rojo Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
* Added fallback method for when an Instance can't be synced through normal means ([#1030])
|
||||
This should make it possible to sync `MeshParts` and `Unions`!
|
||||
|
||||
The fallback involves deleting and recreating Instances. This will break
|
||||
properties that reference them that Rojo does not know about, so be weary.
|
||||
|
||||
* Add auto-reconnect and improve UX for sync reminders ([#1096])
|
||||
* Add support for syncing `yml` and `yaml` files (behaves similar to JSON and TOML) ([#1093])
|
||||
* Fixed colors of Table diff ([#1084])
|
||||
* Fixed `sourcemap` command outputting paths with OS-specific path separators ([#1085])
|
||||
* Fixed nil -> nil properties showing up as failing to sync in plugin's patch visualizer ([#1081])
|
||||
* Changed the background of the server's in-browser UI to be gray instead of white ([#1080])
|
||||
* Fixed `Auto Connect Playtest Server` no longer functioning due to Roblox change ([#1066])
|
||||
* Added an update indicator to the version header when a new version of the plugin is available. ([#1069])
|
||||
* Added `--absolute` flag to the sourcemap subcommand, which will emit absolute paths instead of relative paths. ([#1092])
|
||||
* Fixed applying `gameId` and `placeId` before initial sync was accepted ([#1104])
|
||||
|
||||
[#1030]: https://github.com/rojo-rbx/rojo/pull/1030
|
||||
[#1096]: https://github.com/rojo-rbx/rojo/pull/1096
|
||||
[#1093]: https://github.com/rojo-rbx/rojo/pull/1093
|
||||
[#1084]: https://github.com/rojo-rbx/rojo/pull/1084
|
||||
[#1085]: https://github.com/rojo-rbx/rojo/pull/1085
|
||||
[#1081]: https://github.com/rojo-rbx/rojo/pull/1081
|
||||
[#1080]: https://github.com/rojo-rbx/rojo/pull/1080
|
||||
[#1066]: https://github.com/rojo-rbx/rojo/pull/1066
|
||||
[#1069]: https://github.com/rojo-rbx/rojo/pull/1069
|
||||
[#1092]: https://github.com/rojo-rbx/rojo/pull/1092
|
||||
[#1104]: https://github.com/rojo-rbx/rojo/pull/1104
|
||||
|
||||
## 7.5.1 - April 25th, 2025
|
||||
* Fixed output spam related to `Instance.Capabilities` in the plugin
|
||||
|
||||
## 7.5.0 - April 25th, 2025
|
||||
* Fixed an edge case that caused model pivots to not be built correctly in some cases ([#1027])
|
||||
* Add `blockedPlaceIds` project config field to allow blocking place ids from being live synced ([#1021])
|
||||
* Adds support for `.plugin.lua(u)` files - this applies the `Plugin` RunContext. ([#1008])
|
||||
* Added support for Roblox's `Content` type. This replaces the old `Content` type with `ContentId` to reflect Roblox's change.
|
||||
If you were previously using the fully-qualified syntax for `Content` you will need to switch it to `ContentId`.
|
||||
* Added support for `Enum` attributes
|
||||
* Significantly improved performance of `.rbxm` parsing
|
||||
* Support for a `$schema` field in all special JSON files (`.project.json`, `.model.json`, and `.meta.json`) ([#974])
|
||||
* 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])
|
||||
* The sync reminder notification will now tell you what was last synced and when ([#987])
|
||||
* Fixed notification and tooltip text sometimes getting cut off ([#988])
|
||||
* 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.
|
||||
|
||||
| `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! |
|
||||
|
||||
Additionally, there are `use` values for specific script types ([#909]):
|
||||
|
||||
| `use` value | script type |
|
||||
|:-------------------------|:---------------------------------------|
|
||||
| `legacyServerScript` | `Script` with `Enum.RunContext.Legacy` |
|
||||
| `legacyClientScript` | `LocalScript` |
|
||||
| `runContextServerScript` | `Script` with `Enum.RunContext.Server` |
|
||||
| `runContextClientScript` | `Script` with `Enum.RunContext.Client` |
|
||||
| `pluginScript` | `Script` with `Enum.RunContext.Plugin` |
|
||||
|
||||
**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
|
||||
[#909]: https://github.com/rojo-rbx/rojo/pull/909
|
||||
[#911]: https://github.com/rojo-rbx/rojo/pull/911
|
||||
[#915]: https://github.com/rojo-rbx/rojo/pull/915
|
||||
[#974]: https://github.com/rojo-rbx/rojo/pull/974
|
||||
[#987]: https://github.com/rojo-rbx/rojo/pull/987
|
||||
[#988]: https://github.com/rojo-rbx/rojo/pull/988
|
||||
[#1008]: https://github.com/rojo-rbx/rojo/pull/1008
|
||||
[#1021]: https://github.com/rojo-rbx/rojo/pull/1021
|
||||
[#1027]: https://github.com/rojo-rbx/rojo/pull/1027
|
||||
## Unreleased Changes
|
||||
|
||||
## [7.4.4] - August 22nd, 2024
|
||||
* Fixed issue with reading attributes from `Lighting` in new place files
|
||||
@@ -155,7 +10,7 @@
|
||||
* 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
|
||||
[#955]: https://github.com/rojo-rbx/rojo/pull/893
|
||||
|
||||
## [7.4.2] - July 23, 2024
|
||||
* Added Never option to Confirmation ([#893])
|
||||
@@ -170,8 +25,7 @@
|
||||
|
||||
## [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
|
||||
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.
|
||||
@@ -727,7 +581,7 @@ This is a general maintenance release for the Rojo 0.5.x release series.
|
||||
|
||||
## [0.5.0 Alpha 11](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.11) (May 29, 2019)
|
||||
* Added support for implicit property values in JSON model files ([#154](https://github.com/rojo-rbx/rojo/pull/154))
|
||||
* `Content` properties can now be specified in projects and model files as regular string literals.
|
||||
* `Content` propertyes can now be specified in projects and model files as regular string literals.
|
||||
* Added support for `BrickColor` properties.
|
||||
* Added support for properties added in client release 384, like `Lighting.Technology` being set to `"ShadowMap"`.
|
||||
* Improved performance when working with XML models and places
|
||||
|
||||
@@ -15,29 +15,12 @@ You'll want these tools to work on Rojo:
|
||||
|
||||
* Latest stable Rust compiler
|
||||
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||
* [Rokit](https://github.com/rojo-rbx/rokit)
|
||||
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
|
||||
|
||||
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
|
||||
|
||||
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
|
||||
|
||||
```bash
|
||||
bash scripts/watch-build-plugin.sh
|
||||
```
|
||||
|
||||
You can also run the plugin's unit tests with the following:
|
||||
|
||||
*(Make sure you have `run-in-roblox` installed first!)*
|
||||
|
||||
```bash
|
||||
bash scripts/unit-test-plugin.sh
|
||||
```
|
||||
* [Foreman](https://github.com/Roblox/foreman)
|
||||
|
||||
## Documentation
|
||||
Documentation impacts way more people than the individual lines of code we write.
|
||||
|
||||
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||
If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||
|
||||
## Bug Reports and Feature Requests
|
||||
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
||||
|
||||
1170
Cargo.lock
generated
98
Cargo.toml
@@ -1,12 +1,8 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.5.1"
|
||||
rust-version = "1.79.0"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
version = "7.4.4"
|
||||
rust-version = "1.70.0"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "Enables professional-grade development tools for Roblox developers"
|
||||
license = "MPL-2.0"
|
||||
homepage = "https://rojo.space"
|
||||
@@ -30,9 +26,7 @@ default = []
|
||||
# Enable this feature to live-reload assets from the web UI.
|
||||
dev_live_assets = []
|
||||
|
||||
# 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"]
|
||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
|
||||
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
@@ -55,46 +49,46 @@ memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = "1.0.0"
|
||||
rbx_dom_weak = "3.0.0"
|
||||
rbx_reflection = "5.0.0"
|
||||
rbx_reflection_database = "1.0.3"
|
||||
rbx_xml = "1.0.0"
|
||||
rbx_binary = "0.7.7"
|
||||
rbx_dom_weak = "2.9.0"
|
||||
rbx_reflection = "4.7.0"
|
||||
rbx_reflection_database = "0.2.12"
|
||||
rbx_xml = "0.13.5"
|
||||
|
||||
anyhow = "1.0.80"
|
||||
backtrace = "0.3.69"
|
||||
anyhow = "1.0.44"
|
||||
backtrace = "0.3.61"
|
||||
bincode = "1.3.3"
|
||||
crossbeam-channel = "0.5.12"
|
||||
csv = "1.3.0"
|
||||
env_logger = "0.9.3"
|
||||
fs-err = "2.11.0"
|
||||
futures = "0.3.30"
|
||||
globset = "0.4.14"
|
||||
crossbeam-channel = "0.5.1"
|
||||
csv = "1.1.6"
|
||||
env_logger = "0.9.0"
|
||||
fs-err = "2.6.0"
|
||||
futures = "0.3.17"
|
||||
globset = "0.4.8"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
|
||||
jod-thread = "0.1.2"
|
||||
log = "0.4.21"
|
||||
num_cpus = "1.16.0"
|
||||
opener = "0.5.2"
|
||||
rayon = "1.9.0"
|
||||
reqwest = { version = "0.11.24", default-features = false, features = [
|
||||
log = "0.4.14"
|
||||
maplit = "1.0.2"
|
||||
num_cpus = "1.15.0"
|
||||
opener = "0.5.0"
|
||||
rayon = "1.7.0"
|
||||
reqwest = { version = "0.11.10", features = [
|
||||
"blocking",
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"native-tls-vendored",
|
||||
] }
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.114"
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.2.25", features = ["derive"] }
|
||||
profiling = "1.0.15"
|
||||
yaml-rust2 = "0.10.3"
|
||||
data-encoding = "2.8.0"
|
||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.68"
|
||||
toml = "0.5.9"
|
||||
termcolor = "1.1.2"
|
||||
thiserror = "1.0.30"
|
||||
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
||||
uuid = { version = "1.0.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.1.18", features = ["derive"] }
|
||||
profiling = "1.0.6"
|
||||
tracy-client = { version = "0.13.2", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.10.1"
|
||||
@@ -102,20 +96,20 @@ winreg = "0.10.1"
|
||||
[build-dependencies]
|
||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
|
||||
embed-resource = "1.8.0"
|
||||
anyhow = "1.0.80"
|
||||
embed-resource = "1.6.4"
|
||||
anyhow = "1.0.44"
|
||||
bincode = "1.3.3"
|
||||
fs-err = "2.11.0"
|
||||
fs-err = "2.6.0"
|
||||
maplit = "1.0.2"
|
||||
semver = "1.0.22"
|
||||
semver = "1.0.19"
|
||||
|
||||
[dev-dependencies]
|
||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||
|
||||
criterion = "0.3.6"
|
||||
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
|
||||
paste = "1.0.14"
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_yaml = "0.8.26"
|
||||
tempfile = "3.10.1"
|
||||
walkdir = "2.5.0"
|
||||
criterion = "0.3.5"
|
||||
insta = { version = "1.8.0", features = ["redactions", "yaml"] }
|
||||
paste = "1.0.5"
|
||||
pretty_assertions = "1.2.1"
|
||||
serde_yaml = "0.8.21"
|
||||
tempfile = "3.2.0"
|
||||
walkdir = "2.3.2"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a>
|
||||
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
@@ -43,4 +43,4 @@ Pull requests are welcome!
|
||||
Rojo supports Rust 1.70.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## 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.
|
||||
5
aftman.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[tools]
|
||||
rojo = "rojo-rbx/rojo@7.4.1"
|
||||
selene = "Kampfkarren/selene@0.26.1"
|
||||
stylua = "JohnnyMorganz/stylua@0.18.2"
|
||||
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: 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 |
|
Before Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 574 B |
|
Before Width: | Height: | Size: 607 B |
@@ -17,10 +17,6 @@ html {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #e7e7e7
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
28
build.rs
@@ -20,10 +20,6 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
|
||||
let file_name = entry.file_name().to_str().unwrap().to_owned();
|
||||
|
||||
if file_name.starts_with(".git") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can skip any TestEZ test files since they aren't necessary for
|
||||
// the plugin to run.
|
||||
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
|
||||
@@ -45,12 +41,12 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
|
||||
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let plugin_dir = root_dir.join("plugin");
|
||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||
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_dir.join("Version.txt"))?.trim())?;
|
||||
Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
|
||||
|
||||
assert_eq!(
|
||||
our_version, plugin_version,
|
||||
@@ -58,16 +54,14 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
);
|
||||
|
||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?,
|
||||
"plugin" => VfsSnapshot::dir(hashmap! {
|
||||
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
|
||||
}),
|
||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||
"fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
|
||||
"http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
|
||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||
"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");
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.3.0"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
"Ken Loeffler <kenloef@gmail.com>",
|
||||
]
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
@@ -15,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
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5.12"
|
||||
fs-err = "2.11.0"
|
||||
notify = "4.0.17"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
crossbeam-channel = "0.5.1"
|
||||
fs-err = "2.3.0"
|
||||
notify = "4.0.15"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -6,5 +6,5 @@ edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.197"
|
||||
serde_yaml = "0.8.26"
|
||||
serde = "1.0.99"
|
||||
serde_yaml = "0.8.9"
|
||||
|
||||
@@ -5,13 +5,19 @@ use serde::Serialize;
|
||||
/// Enables redacting any value that serializes as a string.
|
||||
///
|
||||
/// Used for transforming Rojo instance IDs into something deterministic.
|
||||
#[derive(Default)]
|
||||
pub struct RedactionMap {
|
||||
ids: HashMap<String, usize>,
|
||||
last_id: usize,
|
||||
}
|
||||
|
||||
impl RedactionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ids: HashMap::new(),
|
||||
last_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
|
||||
let id = id.to_string();
|
||||
|
||||
@@ -22,12 +28,6 @@ impl RedactionMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the numeric ID that was assigned to the provided value,
|
||||
/// if one exists.
|
||||
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
|
||||
self.ids.get(&value.to_string()).cloned()
|
||||
}
|
||||
|
||||
pub fn intern(&mut self, id: impl ToString) {
|
||||
let last_id = &mut self.last_id;
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
7.5.1
|
||||
7.4.4
|
||||
@@ -3,25 +3,25 @@
|
||||
"tree": {
|
||||
"$className": "Folder",
|
||||
"Plugin": {
|
||||
"$path": "plugin/src"
|
||||
"$path": "src"
|
||||
},
|
||||
"Packages": {
|
||||
"$path": "plugin/Packages",
|
||||
"$path": "Packages",
|
||||
"Log": {
|
||||
"$path": "plugin/log"
|
||||
"$path": "log"
|
||||
},
|
||||
"Http": {
|
||||
"$path": "plugin/http"
|
||||
"$path": "http"
|
||||
},
|
||||
"Fmt": {
|
||||
"$path": "plugin/fmt"
|
||||
"$path": "fmt"
|
||||
},
|
||||
"RbxDom": {
|
||||
"$path": "plugin/rbx_dom_lua"
|
||||
"$path": "rbx_dom_lua"
|
||||
}
|
||||
},
|
||||
"Version": {
|
||||
"$path": "plugin/Version.txt"
|
||||
"$path": "Version.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
|
||||
elseif valueType == "table" then
|
||||
local valueMeta = getmetatable(value)
|
||||
|
||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||
-- This type implement's the metamethod we made up to line up with
|
||||
-- Rust's 'Debug' trait.
|
||||
|
||||
@@ -242,4 +242,4 @@ return {
|
||||
debugOutputBuffer = debugOutputBuffer,
|
||||
fmt = fmt,
|
||||
debugify = debugify,
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,4 @@ function Response:json()
|
||||
return HttpService:JSONDecode(self.body)
|
||||
end
|
||||
|
||||
return Response
|
||||
return Response
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -57,4 +57,4 @@ function Log.error(template, ...)
|
||||
error(Fmt.fmt(template, ...))
|
||||
end
|
||||
|
||||
return Log
|
||||
return Log
|
||||
@@ -2,4 +2,4 @@ return function()
|
||||
it("should load", function()
|
||||
require(script.Parent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -188,38 +188,6 @@ types = {
|
||||
},
|
||||
|
||||
Content = {
|
||||
fromPod = function(pod): Content
|
||||
if type(pod) == "string" then
|
||||
if pod == "None" then
|
||||
return Content.none
|
||||
else
|
||||
error(`unexpected Content value '{pod}'`)
|
||||
end
|
||||
else
|
||||
local ty, value = next(pod)
|
||||
if ty == "Uri" then
|
||||
return Content.fromUri(value)
|
||||
elseif ty == "Object" then
|
||||
error("Object deserializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{ty}' (could not deserialize)`)
|
||||
end
|
||||
end
|
||||
end,
|
||||
toPod = function(roblox: Content)
|
||||
if roblox.SourceType == Enum.ContentSourceType.None then
|
||||
return "None"
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
|
||||
return { Uri = roblox.Uri }
|
||||
elseif roblox.SourceType == Enum.ContentSourceType.Object then
|
||||
error("Object serializing is not currently implemented")
|
||||
else
|
||||
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
||||
ContentId = {
|
||||
fromPod = identity,
|
||||
toPod = identity,
|
||||
},
|
||||
@@ -237,19 +205,6 @@ types = {
|
||||
end,
|
||||
},
|
||||
|
||||
EnumItem = {
|
||||
fromPod = function(pod)
|
||||
return Enum[pod.type]:FromValue(pod.value)
|
||||
end,
|
||||
|
||||
toPod = function(roblox)
|
||||
return {
|
||||
type = tostring(roblox.EnumType),
|
||||
value = roblox.Value,
|
||||
}
|
||||
end,
|
||||
},
|
||||
|
||||
Faces = {
|
||||
fromPod = function(pod)
|
||||
local faces = {}
|
||||
@@ -345,12 +300,7 @@ types = {
|
||||
local keypoints = {}
|
||||
|
||||
for index, keypoint in ipairs(pod.keypoints) do
|
||||
-- TODO: Add a test for NaN or Infinity values and envelopes
|
||||
-- Right now it isn't possible because it'd fail the roundtrip.
|
||||
-- It's more important that it works right now, though.
|
||||
local value = keypoint.value or 0
|
||||
local envelope = keypoint.envelope or 0
|
||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
|
||||
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
|
||||
end
|
||||
|
||||
return NumberSequence.new(keypoints)
|
||||
|
||||
@@ -5,7 +5,6 @@ Error.Kind = {
|
||||
UnknownProperty = "UnknownProperty",
|
||||
PropertyNotReadable = "PropertyNotReadable",
|
||||
PropertyNotWritable = "PropertyNotWritable",
|
||||
CannotParseBinaryString = "CannotParseBinaryString",
|
||||
Roblox = "Roblox",
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"TestEnumItem": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"TestNumber": {
|
||||
"Float64": 1337.0
|
||||
},
|
||||
@@ -176,23 +170,9 @@
|
||||
},
|
||||
"ty": "ColorSequence"
|
||||
},
|
||||
"ContentId": {
|
||||
"Content": {
|
||||
"value": {
|
||||
"ContentId": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "ContentId"
|
||||
},
|
||||
"Content_None": {
|
||||
"value": {
|
||||
"Content": "None"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
"Content_Uri": {
|
||||
"value": {
|
||||
"Content": {
|
||||
"Uri": "rbxasset://abc/123.rojo"
|
||||
}
|
||||
"Content": "rbxassetid://12345"
|
||||
},
|
||||
"ty": "Content"
|
||||
},
|
||||
@@ -202,15 +182,6 @@
|
||||
},
|
||||
"ty": "Enum"
|
||||
},
|
||||
"EnumItem": {
|
||||
"value": {
|
||||
"EnumItem": {
|
||||
"type": "Material",
|
||||
"value": 256
|
||||
}
|
||||
},
|
||||
"ty": "EnumItem"
|
||||
},
|
||||
"Faces": {
|
||||
"value": {
|
||||
"Faces": [
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
local CollectionService = game:GetService("CollectionService")
|
||||
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||
local TERRAIN_MATERIAL_COLORS = {
|
||||
Enum.Material.Grass,
|
||||
@@ -53,10 +51,6 @@ return {
|
||||
return true, instance:GetAttributes()
|
||||
end,
|
||||
write = function(instance, _, value)
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetAttributes()
|
||||
local didAllWritesSucceed = true
|
||||
|
||||
@@ -166,14 +160,9 @@ return {
|
||||
return true, colors
|
||||
end,
|
||||
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
for material, color in value do
|
||||
instance:SetMaterialColor(material, color)
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10))
|
||||
local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
|
||||
|
||||
local Rojo = ReplicatedStorage.Rojo
|
||||
|
||||
@@ -8,12 +8,4 @@ local Settings = require(Rojo.Plugin.Settings)
|
||||
Settings:set("logLevel", "Trace")
|
||||
Settings:set("typecheckingEnabled", true)
|
||||
|
||||
local results = require(Rojo.Plugin.runTests)(TestEZ)
|
||||
|
||||
-- Roblox's Luau execution gets mad about cyclical tables.
|
||||
-- Rather than making TestEZ not do that, we just send back the important info.
|
||||
return {
|
||||
failureCount = results.failureCount,
|
||||
successCount = results.successCount,
|
||||
skippedCount = results.skippedCount,
|
||||
}
|
||||
require(Rojo.Plugin.runTests)(TestEZ)
|
||||
|
||||
@@ -10,8 +10,6 @@ local Version = require(script.Parent.Version)
|
||||
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
|
||||
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
|
||||
|
||||
local function rejectFailedRequests(response)
|
||||
if response.code >= 400 then
|
||||
@@ -47,7 +45,14 @@ end
|
||||
|
||||
local function rejectWrongPlaceId(infoResponseBody)
|
||||
if infoResponseBody.expectedPlaceIds ~= nil then
|
||||
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
|
||||
local foundId = false
|
||||
|
||||
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
|
||||
if id == game.PlaceId then
|
||||
foundId = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not foundId then
|
||||
local idList = {}
|
||||
@@ -57,30 +62,10 @@ local function rejectWrongPlaceId(infoResponseBody)
|
||||
|
||||
local message = (
|
||||
"Found a Rojo server, but its project is set to only be used with a specific list of places."
|
||||
.. "\nYour place ID is %u, but needs to be one of these:"
|
||||
.. "\nYour place ID is %s, but needs to be one of these:"
|
||||
.. "\n%s"
|
||||
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
||||
):format(game.PlaceId, table.concat(idList, "\n"))
|
||||
|
||||
return Promise.reject(message)
|
||||
end
|
||||
end
|
||||
|
||||
if infoResponseBody.unexpectedPlaceIds ~= nil then
|
||||
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
|
||||
|
||||
if foundId then
|
||||
local idList = {}
|
||||
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
|
||||
table.insert(idList, "- " .. tostring(id))
|
||||
end
|
||||
|
||||
local message = (
|
||||
"Found a Rojo server, but its project is set to not be used with a specific list of places."
|
||||
.. "\nYour place ID is %u, but needs to not be one of these:"
|
||||
.. "\n%s"
|
||||
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
|
||||
):format(game.PlaceId, table.concat(idList, "\n"))
|
||||
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
|
||||
|
||||
return Promise.reject(message)
|
||||
end
|
||||
@@ -254,32 +239,4 @@ function ApiContext:open(id)
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:serialize(ids: { string })
|
||||
local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiSerialize(body))
|
||||
|
||||
return body
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:refPatch(ids: { string })
|
||||
local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiRefPatch(body))
|
||||
|
||||
return body
|
||||
end)
|
||||
end
|
||||
|
||||
return ApiContext
|
||||
|
||||
@@ -32,7 +32,7 @@ end
|
||||
|
||||
function Checkbox:render()
|
||||
return Theme.with(function(theme)
|
||||
local checkboxTheme = theme.Checkbox
|
||||
theme = theme.Checkbox
|
||||
|
||||
local activeTransparency = Roact.joinBindings({
|
||||
self.binding:map(function(value)
|
||||
@@ -57,21 +57,20 @@ function Checkbox:render()
|
||||
end,
|
||||
}, {
|
||||
StateTip = e(Tooltip.Trigger, {
|
||||
text = (if self.props.locked
|
||||
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n"
|
||||
else "") .. (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, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = checkboxTheme.Active.BackgroundColor,
|
||||
color = theme.Active.BackgroundColor,
|
||||
transparency = activeTransparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
zIndex = 2,
|
||||
}, {
|
||||
Icon = e("ImageLabel", {
|
||||
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
||||
ImageColor3 = checkboxTheme.Active.IconColor,
|
||||
ImageColor3 = theme.Active.IconColor,
|
||||
ImageTransparency = activeTransparency,
|
||||
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
@@ -84,7 +83,7 @@ function Checkbox:render()
|
||||
|
||||
Inactive = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = checkboxTheme.Inactive.BorderColor,
|
||||
color = theme.Inactive.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
@@ -92,7 +91,7 @@ function Checkbox:render()
|
||||
Image = if self.props.locked
|
||||
then Assets.Images.Checkbox.Locked
|
||||
else Assets.Images.Checkbox.Inactive,
|
||||
ImageColor3 = checkboxTheme.Inactive.IconColor,
|
||||
ImageColor3 = theme.Inactive.IconColor,
|
||||
ImageTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(0, 16, 0, 16),
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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
|
||||
@@ -1,5 +1,4 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
@@ -8,8 +7,6 @@ Highlighter.matchStudioSettings()
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
|
||||
|
||||
function CodeLabel:init()
|
||||
@@ -43,24 +40,22 @@ function CodeLabel:updateHighlights()
|
||||
end
|
||||
|
||||
function CodeLabel:render()
|
||||
return Theme.with(function(theme)
|
||||
return e("TextLabel", {
|
||||
Size = self.props.size,
|
||||
Position = self.props.position,
|
||||
Text = self.props.text,
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
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 e("TextLabel", {
|
||||
Size = self.props.size,
|
||||
Position = self.props.position,
|
||||
Text = self.props.text,
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.RobotoMono,
|
||||
TextSize = 16,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Ref] = self.labelRef,
|
||||
}, {
|
||||
SyntaxHighlights = e("Folder", {
|
||||
[Roact.Ref] = self.highlightsRef,
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return CodeLabel
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -8,11 +10,9 @@ local Flipper = require(Packages.Flipper)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
||||
local Tooltip = require(script.Parent.Tooltip)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -44,29 +44,29 @@ end
|
||||
|
||||
function Dropdown:render()
|
||||
return Theme.with(function(theme)
|
||||
local dropdownTheme = theme.Dropdown
|
||||
theme = theme.Dropdown
|
||||
|
||||
local optionButtons = {}
|
||||
local width = -1
|
||||
for i, option in self.props.options do
|
||||
local text = tostring(option or "")
|
||||
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge)
|
||||
if textBounds.X > width then
|
||||
width = textBounds.X
|
||||
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
|
||||
if textSize.X > width then
|
||||
width = textSize.X
|
||||
end
|
||||
|
||||
optionButtons[text] = e("TextButton", {
|
||||
Text = text,
|
||||
LayoutOrder = i,
|
||||
Size = UDim2.new(1, 0, 0, 24),
|
||||
BackgroundColor3 = dropdownTheme.BackgroundColor,
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
BackgroundTransparency = self.props.transparency,
|
||||
BorderSizePixel = 0,
|
||||
TextColor3 = dropdownTheme.TextColor,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextSize = theme.TextSize.Body,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = 15,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
if self.props.locked then
|
||||
@@ -103,13 +103,15 @@ function Dropdown:render()
|
||||
}, {
|
||||
Border = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = dropdownTheme.BorderColor,
|
||||
color = theme.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
DropArrow = e("ImageLabel", {
|
||||
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
||||
ImageColor3 = dropdownTheme.IconColor,
|
||||
ImageColor3 = self.openBinding:map(function(a)
|
||||
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
|
||||
end),
|
||||
ImageTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(0, 18, 0, 18),
|
||||
@@ -120,21 +122,15 @@ function Dropdown:render()
|
||||
end),
|
||||
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
StateTip = if self.props.locked
|
||||
then e(Tooltip.Trigger, {
|
||||
text = self.props.lockedTooltip or "(Cannot be changed right now)",
|
||||
})
|
||||
else nil,
|
||||
}),
|
||||
Active = e("TextLabel", {
|
||||
Size = UDim2.new(1, -30, 1, 0),
|
||||
Position = UDim2.new(0, 6, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Text = self.props.active,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = dropdownTheme.TextColor,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
}),
|
||||
@@ -142,7 +138,7 @@ function Dropdown:render()
|
||||
Options = if self.state.open
|
||||
then e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = dropdownTheme.BackgroundColor,
|
||||
color = theme.BackgroundColor,
|
||||
position = UDim2.new(1, 0, 1, 3),
|
||||
size = self.openBinding:map(function(a)
|
||||
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
||||
@@ -151,7 +147,7 @@ function Dropdown:render()
|
||||
}, {
|
||||
Border = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = dropdownTheme.BorderColor,
|
||||
color = theme.BorderColor,
|
||||
transparency = self.props.transparency,
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}),
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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
|
||||
@@ -9,70 +9,8 @@ local Assets = require(Plugin.Assets)
|
||||
local Config = require(Plugin.Config)
|
||||
local Version = require(Plugin.Version)
|
||||
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local function VersionIndicator(props)
|
||||
local updateMessage = Version.getUpdateMessage()
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
LayoutOrder = props.layoutOrder,
|
||||
Size = UDim2.new(0, 0, 0, 25),
|
||||
BackgroundTransparency = 1,
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
}, {
|
||||
Border = if updateMessage
|
||||
then e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = theme.Button.Bordered.Enabled.BorderColor,
|
||||
transparency = props.transparency,
|
||||
size = UDim2.fromScale(1, 1),
|
||||
zIndex = 0,
|
||||
}, {
|
||||
Indicator = e("ImageLabel", {
|
||||
Size = UDim2.new(0, 10, 0, 10),
|
||||
ScaleType = Enum.ScaleType.Fit,
|
||||
Image = Assets.Images.Circles[16],
|
||||
ImageColor3 = theme.Header.LogoColor,
|
||||
ImageTransparency = props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.new(1, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
}),
|
||||
})
|
||||
else nil,
|
||||
|
||||
Tip = if updateMessage
|
||||
then e(Tooltip.Trigger, {
|
||||
text = updateMessage,
|
||||
delay = 0.1,
|
||||
})
|
||||
else nil,
|
||||
|
||||
VersionText = e("TextLabel", {
|
||||
Text = Version.display(Config.version),
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Header.VersionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 6),
|
||||
PaddingRight = UDim.new(0, 6),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
local function Header(props)
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
@@ -91,9 +29,18 @@ local function Header(props)
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
VersionIndicator = e(VersionIndicator, {
|
||||
transparency = props.transparency,
|
||||
layoutOrder = 2,
|
||||
Version = e("TextLabel", {
|
||||
Text = Version.display(Config.version),
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Header.VersionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 14),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotificationnNotification")
|
||||
|
||||
function FullscreenNotification:init()
|
||||
self.transparency, self.setTransparency = Roact.createBinding(0)
|
||||
self.lifetime = self.props.timeout
|
||||
end
|
||||
|
||||
function FullscreenNotification:dismiss()
|
||||
if self.props.onClose then
|
||||
self.props.onClose()
|
||||
end
|
||||
end
|
||||
|
||||
function FullscreenNotification:didMount()
|
||||
self.props.soundPlayer:play(Assets.Sounds.Notification)
|
||||
|
||||
self.timeout = task.spawn(function()
|
||||
local clock = os.clock()
|
||||
local seen = false
|
||||
while task.wait(1 / 10) do
|
||||
local now = os.clock()
|
||||
local dt = now - clock
|
||||
clock = now
|
||||
|
||||
if not seen then
|
||||
seen = StudioService.ActiveScript == nil
|
||||
end
|
||||
|
||||
if not seen then
|
||||
-- Don't run down timer before being viewed
|
||||
continue
|
||||
end
|
||||
|
||||
self.lifetime -= dt
|
||||
if self.lifetime <= 0 then
|
||||
self:dismiss()
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
self.timeout = nil
|
||||
end)
|
||||
end
|
||||
|
||||
function FullscreenNotification:willUnmount()
|
||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
end
|
||||
|
||||
function FullscreenNotification:render()
|
||||
return Theme.with(function(theme)
|
||||
local actionButtons = {}
|
||||
if self.props.actions then
|
||||
for key, action in self.props.actions do
|
||||
actionButtons[key] = e(TextButton, {
|
||||
text = action.text,
|
||||
style = action.style,
|
||||
onClick = function()
|
||||
self:dismiss()
|
||||
if action.onClick then
|
||||
local success, err = pcall(action.onClick, self)
|
||||
if not success then
|
||||
Log.warn("Error in notification action: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end,
|
||||
layoutOrder = -action.layoutOrder,
|
||||
transparency = self.transparency,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
BackgroundColor3 = theme.BackgroundColor,
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
ZIndex = self.props.layoutOrder,
|
||||
}, {
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 17),
|
||||
PaddingRight = UDim.new(0, 15),
|
||||
PaddingTop = UDim.new(0, 10),
|
||||
PaddingBottom = UDim.new(0, 10),
|
||||
}),
|
||||
Layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
Logo = e("ImageLabel", {
|
||||
ImageTransparency = self.transparency,
|
||||
Image = Assets.Images.Logo,
|
||||
ImageColor3 = theme.Header.LogoColor,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.fromOffset(60, 27),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
Info = e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Notification.InfoColor,
|
||||
TextTransparency = self.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
TextWrapped = true,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
AutomaticSize = Enum.AutomaticSize.Y,
|
||||
Size = UDim2.fromScale(0.4, 0),
|
||||
LayoutOrder = 2,
|
||||
}),
|
||||
Actions = if self.props.actions
|
||||
then e("Frame", {
|
||||
Size = UDim2.new(1, -40, 0, 37),
|
||||
BackgroundTransparency = 1,
|
||||
LayoutOrder = 3,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
Buttons = Roact.createFragment(actionButtons),
|
||||
})
|
||||
else nil,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return FullscreenNotification
|
||||
@@ -1,66 +0,0 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
|
||||
local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Notification = require(script.Notification)
|
||||
local FullscreenNotification = require(script.FullscreenNotification)
|
||||
|
||||
local Notifications = Roact.Component:extend("Notifications")
|
||||
|
||||
function Notifications:render()
|
||||
local popupNotifs = {}
|
||||
local fullscreenNotifs = {}
|
||||
|
||||
for id, notif in self.props.notifications do
|
||||
local targetTable = if notif.isFullscreen then fullscreenNotifs else popupNotifs
|
||||
local targetComponent = if notif.isFullscreen then FullscreenNotification else Notification
|
||||
targetTable["NotifID_" .. id] = e(targetComponent, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
text = notif.text,
|
||||
timeout = notif.timeout,
|
||||
actions = notif.actions,
|
||||
layoutOrder = id,
|
||||
onClose = function()
|
||||
if notif.onClose then
|
||||
notif.onClose()
|
||||
end
|
||||
self.props.onClose(id)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Fullscreen = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
notifs = Roact.createFragment(fullscreenNotifs),
|
||||
}),
|
||||
Popups = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 5),
|
||||
PaddingBottom = UDim.new(0, 5),
|
||||
PaddingLeft = UDim.new(0, 5),
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}),
|
||||
notifs = Roact.createFragment(popupNotifs),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
return Notifications
|
||||
@@ -14,123 +14,6 @@ local EMPTY_TABLE = {}
|
||||
|
||||
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,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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")
|
||||
|
||||
function ChangeList:init()
|
||||
@@ -153,9 +36,8 @@ function ChangeList:render()
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}
|
||||
|
||||
local headerRow = changes[1]
|
||||
local headers = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 24),
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = rowTransparency,
|
||||
BackgroundColor3 = theme.Diff.Row,
|
||||
LayoutOrder = 0,
|
||||
@@ -167,36 +49,36 @@ function ChangeList:render()
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
ColumnA = e("TextLabel", {
|
||||
Text = tostring(headerRow[1]),
|
||||
A = e("TextLabel", {
|
||||
Text = tostring(changes[1][1]),
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.3, 0, 1, 0),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
ColumnB = e("TextLabel", {
|
||||
Text = tostring(headerRow[2]),
|
||||
B = e("TextLabel", {
|
||||
Text = tostring(changes[1][2]),
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 2,
|
||||
}),
|
||||
ColumnC = e("TextLabel", {
|
||||
Text = tostring(headerRow[3]),
|
||||
C = e("TextLabel", {
|
||||
Text = tostring(changes[1][3]),
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
@@ -213,8 +95,91 @@ function ChangeList:render()
|
||||
local metadata = values[4] or EMPTY_TABLE
|
||||
local isWarning = metadata.isWarning
|
||||
|
||||
-- Special case for .Source updates
|
||||
-- because we want to display a syntax highlighted diff for better UX
|
||||
if self.props.showSourceDiff and tostring(values[1]) == "Source" then
|
||||
rows[row] = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||
BackgroundColor3 = theme.Diff.Row,
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = row,
|
||||
}, {
|
||||
Padding = e("UIPadding", pad),
|
||||
Layout = e("UIListLayout", {
|
||||
FillDirection = Enum.FillDirection.Horizontal,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
A = e("TextLabel", {
|
||||
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.3, 0, 1, 0),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
Button = e("TextButton", {
|
||||
Text = "",
|
||||
Size = UDim2.new(0.7, 0, 1, -4),
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
[Roact.Event.Activated] = function()
|
||||
if props.showSourceDiff then
|
||||
props.showSourceDiff(tostring(values[2]), tostring(values[3]))
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
e(BorderedContainer, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
transparency = self.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 = self.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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
continue
|
||||
end
|
||||
|
||||
rows[row] = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 24),
|
||||
Size = UDim2.new(1, 0, 0, 30),
|
||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||
BackgroundColor3 = theme.Diff.Row,
|
||||
BorderSizePixel = 0,
|
||||
@@ -227,25 +192,44 @@ function ChangeList:render()
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
}),
|
||||
ColumnA = e("TextLabel", {
|
||||
A = e("TextLabel", {
|
||||
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(0.3, 0, 1, 0),
|
||||
LayoutOrder = 1,
|
||||
}),
|
||||
Content = e(RowContent, {
|
||||
values = values,
|
||||
metadata = metadata,
|
||||
transparency = props.transparency,
|
||||
showStringDiff = props.showStringDiff,
|
||||
showTableDiff = props.showTableDiff,
|
||||
}),
|
||||
B = e(
|
||||
"Frame",
|
||||
{
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 2,
|
||||
},
|
||||
e(DisplayValue, {
|
||||
value = values[2],
|
||||
transparency = props.transparency,
|
||||
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||
})
|
||||
),
|
||||
C = e(
|
||||
"Frame",
|
||||
{
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0.35, 0, 1, 0),
|
||||
LayoutOrder = 3,
|
||||
},
|
||||
e(DisplayValue, {
|
||||
value = values[3],
|
||||
transparency = props.transparency,
|
||||
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||
})
|
||||
),
|
||||
})
|
||||
end
|
||||
|
||||
@@ -269,8 +253,8 @@ function ChangeList:render()
|
||||
}, {
|
||||
Headers = headers,
|
||||
Values = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, -24),
|
||||
position = UDim2.new(0, 0, 0, 24),
|
||||
size = UDim2.new(1, 0, 1, -30),
|
||||
position = UDim2.new(0, 0, 0, 30),
|
||||
contentSize = self.contentSize,
|
||||
transparency = props.transparency,
|
||||
}, rows),
|
||||
|
||||
@@ -30,10 +30,10 @@ local function DisplayValue(props)
|
||||
}),
|
||||
}),
|
||||
Label = e("TextLabel", {
|
||||
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||
Text = string.format("%d,%d,%d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = props.textColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -90,8 +90,8 @@ local function DisplayValue(props)
|
||||
return e("TextLabel", {
|
||||
Text = textRepresentation,
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = props.textColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -104,16 +104,11 @@ local function DisplayValue(props)
|
||||
-- Or special text handling tostring for some?
|
||||
-- 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", {
|
||||
Text = textRepresentation,
|
||||
Text = string.gsub(tostring(props.value), "%s", " "),
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = props.textColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local SelectionService = game:GetService("Selection")
|
||||
local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
@@ -14,8 +15,7 @@ local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local e = Roact.createElement
|
||||
|
||||
local ChangeList = require(script.Parent.ChangeList)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local ClassIcon = require(Plugin.App.Components.ClassIcon)
|
||||
local Tooltip = require(script.Parent.Parent.Tooltip)
|
||||
|
||||
local Expansion = Roact.Component:extend("Expansion")
|
||||
|
||||
@@ -28,14 +28,13 @@ function Expansion:render()
|
||||
|
||||
return e("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, -props.indent, 1, -24),
|
||||
Position = UDim2.new(0, props.indent, 0, 24),
|
||||
Size = UDim2.new(1, -props.indent, 1, -30),
|
||||
Position = UDim2.new(0, props.indent, 0, 30),
|
||||
}, {
|
||||
ChangeList = e(ChangeList, {
|
||||
changes = props.changeList,
|
||||
transparency = props.transparency,
|
||||
showStringDiff = props.showStringDiff,
|
||||
showTableDiff = props.showTableDiff,
|
||||
showSourceDiff = props.showSourceDiff,
|
||||
}),
|
||||
})
|
||||
end
|
||||
@@ -44,7 +43,7 @@ local DomLabel = Roact.Component:extend("DomLabel")
|
||||
|
||||
function DomLabel:init()
|
||||
local initHeight = self.props.elementHeight:getValue()
|
||||
self.expanded = initHeight > 24
|
||||
self.expanded = initHeight > 30
|
||||
|
||||
self.motor = Flipper.SingleMotor.new(initHeight)
|
||||
self.binding = bindingUtil.fromMotor(self.motor)
|
||||
@@ -53,7 +52,7 @@ function DomLabel:init()
|
||||
renderExpansion = self.expanded,
|
||||
})
|
||||
self.motor:onStep(function(value)
|
||||
local renderExpansion = value > 24
|
||||
local renderExpansion = value > 30
|
||||
|
||||
self.props.setElementHeight(value)
|
||||
if self.props.updateEvent then
|
||||
@@ -81,7 +80,7 @@ function DomLabel:didUpdate(prevProps)
|
||||
then
|
||||
-- Close the expansion when the domlabel is changed to a different thing
|
||||
self.expanded = false
|
||||
self.motor:setGoal(Flipper.Spring.new(24, {
|
||||
self.motor:setGoal(Flipper.Spring.new(30, {
|
||||
frequency = 5,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
@@ -90,49 +89,17 @@ end
|
||||
|
||||
function DomLabel:render()
|
||||
local props = self.props
|
||||
local depth = props.depth or 1
|
||||
|
||||
return Theme.with(function(theme)
|
||||
local color = if props.isWarning
|
||||
then theme.Diff.Warning
|
||||
elseif props.patchType then theme.Diff.Background[props.patchType]
|
||||
else theme.TextColor
|
||||
|
||||
local indent = (depth - 1) * 12 + 15
|
||||
local iconProps = StudioService:GetClassIcon(props.className)
|
||||
local indent = (props.depth or 0) * 20 + 25
|
||||
|
||||
-- Line guides help indent depth remain readable
|
||||
local lineGuides = {}
|
||||
for i = 2, depth do
|
||||
if props.depthsComplete[i] then
|
||||
continue
|
||||
end
|
||||
if props.isFinalChild and i == depth then
|
||||
-- This line stops halfway down to merge with our connector for the right angle
|
||||
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,
|
||||
for i = 1, props.depth or 0 do
|
||||
lineGuides["Line_" .. i] = e("Frame", {
|
||||
Size = UDim2.new(0, 2, 1, 2),
|
||||
Position = UDim2.new(0, (20 * i) + 15, 0, -1),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = props.transparency,
|
||||
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||
@@ -141,8 +108,9 @@ function DomLabel:render()
|
||||
|
||||
return e("Frame", {
|
||||
ClipsDescendants = true,
|
||||
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1,
|
||||
BackgroundColor3 = theme.Diff.Row,
|
||||
BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = props.patchType and props.transparency or 1,
|
||||
Size = self.binding:map(function(expand)
|
||||
return UDim2.new(1, 0, 0, expand)
|
||||
end),
|
||||
@@ -172,8 +140,8 @@ function DomLabel:render()
|
||||
|
||||
if props.changeList then
|
||||
self.expanded = not self.expanded
|
||||
local goalHeight = 24
|
||||
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0)
|
||||
local goalHeight = 30
|
||||
+ (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
|
||||
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
|
||||
frequency = 5,
|
||||
dampingRatio = 1,
|
||||
@@ -198,81 +166,46 @@ function DomLabel:render()
|
||||
indent = indent,
|
||||
transparency = props.transparency,
|
||||
changeList = props.changeList,
|
||||
showStringDiff = props.showStringDiff,
|
||||
showTableDiff = props.showTableDiff,
|
||||
showSourceDiff = props.showSourceDiff,
|
||||
})
|
||||
else nil,
|
||||
DiffIcon = if props.patchType
|
||||
then e("ImageLabel", {
|
||||
Image = Assets.Images.Diff[props.patchType],
|
||||
ImageColor3 = color,
|
||||
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
||||
ImageTransparency = props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 14, 0, 14),
|
||||
Position = UDim2.new(0, 0, 0, 12),
|
||||
Size = UDim2.new(0, 20, 0, 20),
|
||||
Position = UDim2.new(0, 0, 0, 15),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
})
|
||||
else nil,
|
||||
ClassIcon = e(ClassIcon, {
|
||||
className = props.className,
|
||||
color = color,
|
||||
transparency = props.transparency,
|
||||
size = UDim2.new(0, 16, 0, 16),
|
||||
position = UDim2.new(0, indent + 2, 0, 12),
|
||||
anchorPoint = Vector2.new(0, 0.5),
|
||||
ClassIcon = e("ImageLabel", {
|
||||
Image = iconProps.Image,
|
||||
ImageTransparency = props.transparency,
|
||||
ImageRectOffset = iconProps.ImageRectOffset,
|
||||
ImageRectSize = iconProps.ImageRectSize,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(0, 20, 0, 20),
|
||||
Position = UDim2.new(0, indent, 0, 15),
|
||||
AnchorPoint = Vector2.new(0, 0.5),
|
||||
}),
|
||||
InstanceName = e("TextLabel", {
|
||||
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
||||
Text = (if props.isWarning then "⚠ " else "") .. props.name .. (props.hint and string.format(
|
||||
' <font color="#%s">%s</font>',
|
||||
theme.AddressEntry.PlaceholderColor:ToHex(),
|
||||
props.hint
|
||||
) or ""),
|
||||
RichText = true,
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = color,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 14,
|
||||
TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
Size = UDim2.new(1, -indent - 50, 0, 24),
|
||||
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,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.SubTextColor,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||
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,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Diff.Warning,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
LayoutOrder = 6,
|
||||
})
|
||||
else nil,
|
||||
Size = UDim2.new(1, -indent - 50, 0, 30),
|
||||
Position = UDim2.new(0, indent + 30, 0, 0),
|
||||
}),
|
||||
LineGuides = e("Folder", nil, lineGuides),
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@ local PatchTree = require(Plugin.PatchTree)
|
||||
local PatchSet = require(Plugin.PatchSet)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -55,60 +55,33 @@ function PatchVisualizer:render()
|
||||
end
|
||||
|
||||
-- Recusively draw tree
|
||||
local scrollElements, elementHeights, elementIndex = {}, {}, 0
|
||||
local scrollElements, elementHeights = {}, {}
|
||||
|
||||
if patchTree then
|
||||
local elementTotal = patchTree:getCount()
|
||||
local depthsComplete = {}
|
||||
local function drawNode(node, depth)
|
||||
elementIndex += 1
|
||||
|
||||
local parentNode = patchTree:getNode(node.parentId)
|
||||
local isFinalChild = true
|
||||
if parentNode then
|
||||
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,
|
||||
elementHeight = elementHeight,
|
||||
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,
|
||||
className = node.className,
|
||||
isWarning = node.isWarning,
|
||||
instance = node.instance,
|
||||
name = node.name,
|
||||
changeInfo = node.changeInfo,
|
||||
changeList = node.changeList,
|
||||
})
|
||||
|
||||
if isFinalChild then
|
||||
depthsComplete[depth] = true
|
||||
end
|
||||
local elementHeight, setElementHeight = Roact.createBinding(30)
|
||||
table.insert(elementHeights, elementHeight)
|
||||
table.insert(
|
||||
scrollElements,
|
||||
e(DomLabel, {
|
||||
updateEvent = self.updateEvent,
|
||||
elementHeight = elementHeight,
|
||||
setElementHeight = setElementHeight,
|
||||
patchType = node.patchType,
|
||||
className = node.className,
|
||||
isWarning = node.isWarning,
|
||||
instance = node.instance,
|
||||
name = node.name,
|
||||
hint = node.hint,
|
||||
changeList = node.changeList,
|
||||
depth = depth,
|
||||
transparency = self.props.transparency,
|
||||
showSourceDiff = self.props.showSourceDiff,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
patchTree:forEach(function(node, depth)
|
||||
depthsComplete[depth] = false
|
||||
for i = depth + 1, #depthsComplete do
|
||||
depthsComplete[i] = nil
|
||||
end
|
||||
|
||||
drawNode(node, depth)
|
||||
end)
|
||||
end
|
||||
@@ -118,23 +91,21 @@ function PatchVisualizer:render()
|
||||
transparency = self.props.transparency,
|
||||
size = self.props.size,
|
||||
position = self.props.position,
|
||||
anchorPoint = self.props.anchorPoint,
|
||||
layoutOrder = self.props.layoutOrder,
|
||||
}, {
|
||||
CleanMerge = e("TextLabel", {
|
||||
Visible = #scrollElements == 0,
|
||||
Text = "No changes to sync, project is up to date.",
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.TextColor,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextWrapped = true,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
VirtualScroller = e(VirtualScroller, {
|
||||
size = UDim2.new(1, 0, 1, -2),
|
||||
position = UDim2.new(0, 0, 0, 2),
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
transparency = self.props.transparency,
|
||||
count = #scrollElements,
|
||||
updateEvent = self.updateEvent.Event,
|
||||
|
||||
@@ -10,12 +10,6 @@ local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
|
||||
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)
|
||||
return Theme.with(function(theme)
|
||||
return e("ScrollingFrame", {
|
||||
@@ -34,21 +28,16 @@ local function ScrollingFrame(props)
|
||||
Size = props.size,
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorPoint,
|
||||
CanvasSize = if props.contentSize
|
||||
then props.contentSize:map(function(value)
|
||||
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,
|
||||
CanvasSize = props.contentSize:map(function(value)
|
||||
return UDim2.new(
|
||||
0,
|
||||
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
|
||||
then value.X
|
||||
else 0,
|
||||
0,
|
||||
value.Y
|
||||
)
|
||||
end),
|
||||
|
||||
BorderSizePixel = 0,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
@@ -20,7 +20,6 @@ local function SlicedImage(props)
|
||||
Size = props.size,
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorPoint,
|
||||
AutomaticSize = props.automaticSize,
|
||||
|
||||
ZIndex = props.zIndex,
|
||||
LayoutOrder = props.layoutOrder,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -7,9 +9,7 @@ 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 getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
@@ -31,6 +31,7 @@ function StringDiffVisualizer:init()
|
||||
end)
|
||||
end)
|
||||
|
||||
self:calculateContentSize()
|
||||
self:updateScriptBackground()
|
||||
|
||||
self:setState({
|
||||
@@ -51,7 +52,8 @@ function StringDiffVisualizer:updateScriptBackground()
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:didUpdate(previousProps)
|
||||
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
||||
if previousProps.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then
|
||||
self:calculateContentSize()
|
||||
local add, remove = self:calculateDiffLines()
|
||||
self:setState({
|
||||
add = add,
|
||||
@@ -60,30 +62,29 @@ function StringDiffVisualizer:didUpdate(previousProps)
|
||||
end
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:calculateContentSize(theme)
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
function StringDiffVisualizer:calculateContentSize()
|
||||
local oldText, newText = self.props.oldText, self.props.newText
|
||||
|
||||
local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||
local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||
local oldTextBounds = TextService:GetTextSize(oldText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||
local newTextBounds = TextService:GetTextSize(newText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||
|
||||
self.setContentSize(
|
||||
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y))
|
||||
Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y))
|
||||
)
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:calculateDiffLines()
|
||||
Timer.start("StringDiffVisualizer:calculateDiffLines")
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
local oldText, newText = self.props.oldText, self.props.newText
|
||||
|
||||
-- Diff the two texts
|
||||
local startClock = os.clock()
|
||||
local diffs = StringDiff.findDiffs(oldString, newString)
|
||||
local diffs = StringDiff.findDiffs(oldText, newText)
|
||||
local stopClock = os.clock()
|
||||
|
||||
Log.trace(
|
||||
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||
#oldString,
|
||||
#newString,
|
||||
#oldText,
|
||||
#newText,
|
||||
math.round((stopClock - startClock) * 1000 * 1000),
|
||||
#diffs
|
||||
)
|
||||
@@ -132,16 +133,13 @@ function StringDiffVisualizer:calculateDiffLines()
|
||||
end
|
||||
end
|
||||
|
||||
Timer.stop()
|
||||
return add, remove
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:render()
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
local oldText, newText = self.props.oldText, self.props.newText
|
||||
|
||||
return Theme.with(function(theme)
|
||||
self:calculateContentSize(theme)
|
||||
|
||||
return e(BorderedContainer, {
|
||||
size = self.props.size,
|
||||
position = self.props.position,
|
||||
@@ -177,8 +175,8 @@ function StringDiffVisualizer:render()
|
||||
Source = e(CodeLabel, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = oldString,
|
||||
lineBackground = theme.Diff.Background.Remove,
|
||||
text = oldText,
|
||||
lineBackground = theme.Diff.Remove,
|
||||
markedLines = self.state.remove,
|
||||
}),
|
||||
}),
|
||||
@@ -192,8 +190,8 @@ function StringDiffVisualizer:render()
|
||||
Source = e(CodeLabel, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = newString,
|
||||
lineBackground = theme.Diff.Background.Add,
|
||||
text = newText,
|
||||
lineBackground = theme.Diff.Add,
|
||||
markedLines = self.state.add,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
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 = theme.Diff.Background[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,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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
|
||||
@@ -1,209 +0,0 @@
|
||||
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 = theme.Diff.Background[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,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.Diff.Text[line.patchType],
|
||||
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.Diff.Text[line.patchType],
|
||||
}),
|
||||
}),
|
||||
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.Diff.Text[line.patchType],
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
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,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
@@ -1,59 +0,0 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
|
||||
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
return function(props)
|
||||
return Theme.with(function(theme)
|
||||
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, theme.TextSize.Medium),
|
||||
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,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Small,
|
||||
TextColor3 = props.color,
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextTransparency = props.transparency,
|
||||
Size = UDim2.new(0, 0, 1, 0),
|
||||
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
|
||||
AutomaticSize = Enum.AutomaticSize.X,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -8,7 +10,6 @@ local Flipper = require(Packages.Flipper)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local SlicedImage = require(script.Parent.SlicedImage)
|
||||
local TouchRipple = require(script.Parent.TouchRipple)
|
||||
@@ -40,17 +41,18 @@ end
|
||||
|
||||
function TextButton:render()
|
||||
return Theme.with(function(theme)
|
||||
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Large, math.huge)
|
||||
local textSize =
|
||||
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
|
||||
|
||||
local style = self.props.style
|
||||
|
||||
local buttonTheme = theme.Button[style]
|
||||
theme = theme.Button[style]
|
||||
|
||||
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||
|
||||
return e("ImageButton", {
|
||||
Size = UDim2.new(0, (theme.TextSize.Body * 2) + textBounds.X, 0, 34),
|
||||
Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34),
|
||||
Position = self.props.position,
|
||||
AnchorPoint = self.props.anchorPoint,
|
||||
|
||||
@@ -72,22 +74,18 @@ function TextButton:render()
|
||||
end,
|
||||
}, {
|
||||
TouchRipple = e(TouchRipple, {
|
||||
color = buttonTheme.ActionFillColor,
|
||||
color = theme.ActionFillColor,
|
||||
transparency = self.props.transparency:map(function(value)
|
||||
return bindingUtil.blendAlpha({ buttonTheme.ActionFillTransparency, value })
|
||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
|
||||
end),
|
||||
zIndex = 2,
|
||||
}),
|
||||
|
||||
Text = e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
buttonTheme.Enabled.TextColor,
|
||||
buttonTheme.Disabled.TextColor
|
||||
),
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 18,
|
||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
||||
TextTransparency = self.props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -97,11 +95,7 @@ function TextButton:render()
|
||||
|
||||
Border = style == "Bordered" and e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBorder,
|
||||
color = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
buttonTheme.Enabled.BorderColor,
|
||||
buttonTheme.Disabled.BorderColor
|
||||
),
|
||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -111,18 +105,14 @@ function TextButton:render()
|
||||
|
||||
HoverOverlay = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = buttonTheme.ActionFillColor,
|
||||
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({
|
||||
buttonTheme.ActionFillTransparency,
|
||||
values.hover,
|
||||
values.transparency,
|
||||
})
|
||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
||||
end),
|
||||
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -134,8 +124,8 @@ function TextButton:render()
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
buttonTheme.Enabled.BackgroundColor,
|
||||
buttonTheme.Disabled.BackgroundColor
|
||||
theme.Enabled.BackgroundColor,
|
||||
theme.Disabled.BackgroundColor
|
||||
),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
|
||||
@@ -38,18 +38,14 @@ end
|
||||
|
||||
function TextInput:render()
|
||||
return Theme.with(function(theme)
|
||||
local textInputTheme = theme.TextInput
|
||||
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,
|
||||
textInputTheme.Enabled.BorderColor,
|
||||
textInputTheme.Disabled.BorderColor
|
||||
),
|
||||
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),
|
||||
@@ -59,18 +55,14 @@ function TextInput:render()
|
||||
}, {
|
||||
HoverOverlay = e(SlicedImage, {
|
||||
slice = Assets.Slices.RoundedBackground,
|
||||
color = textInputTheme.ActionFillColor,
|
||||
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({
|
||||
textInputTheme.ActionFillTransparency,
|
||||
values.hover,
|
||||
values.transparency,
|
||||
})
|
||||
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
||||
end),
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
zIndex = -1,
|
||||
@@ -80,18 +72,14 @@ function TextInput:render()
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
Text = self.props.text,
|
||||
PlaceholderText = self.props.placeholder,
|
||||
FontFace = theme.Font.Main,
|
||||
TextColor3 = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
textInputTheme.Disabled.TextColor,
|
||||
textInputTheme.Enabled.TextColor
|
||||
),
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
|
||||
PlaceholderColor3 = bindingUtil.mapLerp(
|
||||
bindingEnabled,
|
||||
textInputTheme.Disabled.PlaceholderColor,
|
||||
textInputTheme.Enabled.PlaceholderColor
|
||||
theme.Disabled.PlaceholderColor,
|
||||
theme.Enabled.PlaceholderColor
|
||||
),
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextSize = 18,
|
||||
TextEditable = self.props.enabled,
|
||||
ClearTextOnFocus = self.props.clearTextOnFocus,
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
@@ -7,8 +8,6 @@ local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
|
||||
local e = Roact.createElement
|
||||
@@ -22,48 +21,50 @@ local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to
|
||||
local TooltipContext = Roact.createContext({})
|
||||
|
||||
local function Popup(props)
|
||||
local textSize = TextService:GetTextSize(
|
||||
props.Text,
|
||||
16,
|
||||
Enum.Font.GothamMedium,
|
||||
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
||||
) + TEXT_PADDING + (Vector2.one * 2)
|
||||
|
||||
local trigger = props.Trigger:getValue()
|
||||
|
||||
local spaceBelow = props.parentSize.Y
|
||||
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
|
||||
|
||||
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||
local displayAbove = spaceBelow < textSize.Y and spaceAbove > spaceBelow
|
||||
|
||||
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
|
||||
local Y = 0
|
||||
|
||||
if displayAbove then
|
||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
||||
else
|
||||
Y = math.min(
|
||||
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||
props.parentSize.Y - textSize.Y
|
||||
)
|
||||
end
|
||||
|
||||
return Theme.with(function(theme)
|
||||
local textXSpace = math.min(props.parentSize.X, 250) - TEXT_PADDING.X
|
||||
local textBounds = getTextBoundsAsync(props.Text, theme.Font.Main, theme.TextSize.Medium, textXSpace)
|
||||
local contentSize = textBounds + TEXT_PADDING + (Vector2.one * 2)
|
||||
|
||||
local trigger = props.Trigger:getValue()
|
||||
|
||||
local spaceBelow = props.parentSize.Y
|
||||
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
|
||||
|
||||
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||
local displayAbove = spaceBelow < contentSize.Y and spaceAbove > spaceBelow
|
||||
|
||||
local X = math.clamp(props.Position.X - X_OFFSET, 0, math.max(props.parentSize.X - contentSize.X, 1))
|
||||
local Y = 0
|
||||
|
||||
if displayAbove then
|
||||
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - contentSize.Y + Y_OVERLAP, 0)
|
||||
else
|
||||
Y = math.min(
|
||||
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||
props.parentSize.Y - contentSize.Y
|
||||
)
|
||||
end
|
||||
|
||||
return e(BorderedContainer, {
|
||||
position = UDim2.fromOffset(X, Y),
|
||||
size = UDim2.fromOffset(contentSize.X, contentSize.Y),
|
||||
size = UDim2.fromOffset(textSize.X, textSize.Y),
|
||||
transparency = props.transparency,
|
||||
}, {
|
||||
Label = e("TextLabel", {
|
||||
BackgroundTransparency = 1,
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.new(1, -TEXT_PADDING.X, 1, -TEXT_PADDING.Y),
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Size = UDim2.fromOffset(textBounds.X, textBounds.Y),
|
||||
Text = props.Text,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = 16,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextWrapped = true,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
}),
|
||||
@@ -71,8 +72,8 @@ local function Popup(props)
|
||||
Tail = e("ImageLabel", {
|
||||
ZIndex = 100,
|
||||
Position = if displayAbove
|
||||
then UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 1, -1)
|
||||
else UDim2.new(0, math.clamp(props.Position.X - X, 6, contentSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||
then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
|
||||
else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Rotation = if displayAbove then 180 else 0,
|
||||
@@ -162,6 +163,7 @@ local Trigger = Roact.Component:extend("TooltipTrigger")
|
||||
function Trigger:init()
|
||||
self.id = HttpService:GenerateGUID(false)
|
||||
self.ref = Roact.createRef()
|
||||
self.mousePos = Vector2.zero
|
||||
self.showingPopup = false
|
||||
|
||||
self.destroy = function()
|
||||
@@ -193,22 +195,18 @@ end
|
||||
function Trigger:isHovering()
|
||||
local rbx = self.ref.current
|
||||
if rbx then
|
||||
return rbx.GuiState == Enum.GuiState.Hover
|
||||
local pos = rbx.AbsolutePosition
|
||||
local size = rbx.AbsoluteSize
|
||||
local mousePos = self.mousePos
|
||||
|
||||
return mousePos.X >= pos.X
|
||||
and mousePos.X <= pos.X + size.X
|
||||
and mousePos.Y >= pos.Y
|
||||
and mousePos.Y <= pos.Y + size.Y
|
||||
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
|
||||
@@ -216,10 +214,10 @@ function Trigger:managePopup()
|
||||
return
|
||||
end
|
||||
|
||||
self.showDelayThread = task.delay(self.props.delay or DELAY, function()
|
||||
self.showDelayThread = task.delay(DELAY, function()
|
||||
self.props.context.addTip(self.id, {
|
||||
Text = self.props.text,
|
||||
Position = self:getMousePos(),
|
||||
Position = self.mousePos,
|
||||
Trigger = self.ref,
|
||||
})
|
||||
self.showDelayThread = nil
|
||||
@@ -236,7 +234,13 @@ function Trigger:managePopup()
|
||||
end
|
||||
|
||||
function Trigger:render()
|
||||
local function recalculate()
|
||||
local function recalculate(rbx)
|
||||
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
|
||||
if not widget then
|
||||
return
|
||||
end
|
||||
self.mousePos = widget:GetRelativeMousePosition()
|
||||
|
||||
self:managePopup()
|
||||
end
|
||||
|
||||
@@ -246,9 +250,11 @@ function Trigger:render()
|
||||
ZIndex = self.props.zIndex or 100,
|
||||
[Roact.Ref] = self.ref,
|
||||
|
||||
[Roact.Change.GuiState] = recalculate,
|
||||
[Roact.Change.AbsolutePosition] = recalculate,
|
||||
[Roact.Change.AbsoluteSize] = recalculate,
|
||||
[Roact.Event.MouseMoved] = recalculate,
|
||||
[Roact.Event.MouseLeave] = recalculate,
|
||||
[Roact.Event.MouseEnter] = recalculate,
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -131,8 +131,8 @@ function VirtualScroller:render()
|
||||
Position = props.position,
|
||||
AnchorPoint = props.anchorPoint,
|
||||
BackgroundTransparency = props.backgroundTransparency or 1,
|
||||
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
|
||||
BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor,
|
||||
BackgroundColor3 = props.backgroundColor3,
|
||||
BorderColor3 = props.borderColor3,
|
||||
CanvasSize = self.totalCanvas:map(function(s)
|
||||
return UDim2.fromOffset(0, s)
|
||||
end),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
local StudioService = game:GetService("StudioService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
@@ -8,14 +9,16 @@ local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local bindingUtil = require(script.Parent.bindingUtil)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
|
||||
local baseClock = DateTime.now().UnixTimestampMillis
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Notification = Roact.Component:extend("Notification")
|
||||
@@ -75,9 +78,7 @@ function Notification:didMount()
|
||||
end
|
||||
|
||||
function Notification:willUnmount()
|
||||
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
task.cancel(self.timeout)
|
||||
end
|
||||
|
||||
function Notification:render()
|
||||
@@ -85,49 +86,51 @@ function Notification:render()
|
||||
return 1 - value
|
||||
end)
|
||||
|
||||
return Theme.with(function(theme)
|
||||
local actionButtons = {}
|
||||
local buttonsX = 0
|
||||
if self.props.actions then
|
||||
local count = 0
|
||||
for key, action in self.props.actions do
|
||||
actionButtons[key] = e(TextButton, {
|
||||
text = action.text,
|
||||
style = action.style,
|
||||
onClick = function()
|
||||
self:dismiss()
|
||||
if action.onClick then
|
||||
local success, err = pcall(action.onClick, self)
|
||||
if not success then
|
||||
Log.warn("Error in notification action: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end,
|
||||
layoutOrder = -action.layoutOrder,
|
||||
transparency = transparency,
|
||||
})
|
||||
local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
|
||||
|
||||
buttonsX += getTextBoundsAsync(action.text, theme.Font.Main, theme.TextSize.Large, math.huge).X + (theme.TextSize.Body * 2)
|
||||
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,
|
||||
})
|
||||
|
||||
count += 1
|
||||
end
|
||||
buttonsX += TextService:GetTextSize(
|
||||
action.text,
|
||||
18,
|
||||
Enum.Font.GothamMedium,
|
||||
Vector2.new(math.huge, math.huge)
|
||||
).X + 30
|
||||
|
||||
buttonsX += (count - 1) * 5
|
||||
count += 1
|
||||
end
|
||||
|
||||
local paddingY, logoSize = 20, 32
|
||||
local actionsY = if self.props.actions then 37 else 0
|
||||
local textXSpace = math.max(250, buttonsX) + 35
|
||||
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Body, textXSpace)
|
||||
local contentX = math.max(textBounds.X, buttonsX)
|
||||
buttonsX += (count - 1) * 5
|
||||
end
|
||||
|
||||
local size = self.binding:map(function(value)
|
||||
return UDim2.fromOffset(
|
||||
(35 + 40 + contentX) * value,
|
||||
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||
)
|
||||
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)
|
||||
return UDim2.fromOffset(
|
||||
(35 + 40 + contentX) * value,
|
||||
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
|
||||
)
|
||||
end)
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return e("TextButton", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = size,
|
||||
@@ -141,31 +144,31 @@ function Notification:render()
|
||||
}, {
|
||||
e(BorderedContainer, {
|
||||
transparency = transparency,
|
||||
size = UDim2.fromScale(1, 1),
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
}, {
|
||||
Contents = e("Frame", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
|
||||
Position = UDim2.new(0, 0, 0, paddingY / 2),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Logo = e("ImageLabel", {
|
||||
ImageTransparency = transparency,
|
||||
Image = Assets.Images.PluginButton,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.fromOffset(logoSize, logoSize),
|
||||
Size = UDim2.new(0, logoSize, 0, logoSize),
|
||||
Position = UDim2.new(0, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0, 0),
|
||||
}),
|
||||
Info = e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Body,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.Notification.InfoColor,
|
||||
TextTransparency = transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Center,
|
||||
TextWrapped = true,
|
||||
|
||||
Size = UDim2.new(0, textBounds.X, 1, -actionsY),
|
||||
Size = UDim2.new(0, textBounds.X, 0, textBounds.Y),
|
||||
Position = UDim2.fromOffset(35, 0),
|
||||
|
||||
LayoutOrder = 1,
|
||||
@@ -173,8 +176,8 @@ function Notification:render()
|
||||
}),
|
||||
Actions = if self.props.actions
|
||||
then e("Frame", {
|
||||
Size = UDim2.new(1, -40, 0, actionsY),
|
||||
Position = UDim2.fromScale(1, 1),
|
||||
Size = UDim2.new(1, -40, 0, 35),
|
||||
Position = UDim2.new(1, 0, 1, 0),
|
||||
AnchorPoint = Vector2.new(1, 1),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
@@ -193,12 +196,32 @@ function Notification:render()
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 17),
|
||||
PaddingRight = UDim.new(0, 15),
|
||||
PaddingTop = UDim.new(0, paddingY / 2),
|
||||
PaddingBottom = UDim.new(0, paddingY / 2),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return Notification
|
||||
local Notifications = Roact.Component:extend("Notifications")
|
||||
|
||||
function Notifications:render()
|
||||
local notifs = {}
|
||||
|
||||
for id, notif in self.props.notifications do
|
||||
notifs["NotifID_" .. id] = e(Notification, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
text = notif.text,
|
||||
timestamp = notif.timestamp,
|
||||
timeout = notif.timeout,
|
||||
actions = notif.actions,
|
||||
layoutOrder = (notif.timestamp - baseClock),
|
||||
onClose = function()
|
||||
self.props.onClose(id)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return Roact.createFragment(notifs)
|
||||
end
|
||||
|
||||
return Notifications
|
||||
@@ -4,16 +4,14 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local PatchTree = require(Plugin.PatchTree)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local Header = require(Plugin.App.Components.Header)
|
||||
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||
local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -24,75 +22,50 @@ function ConfirmingPage:init()
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
self:setState({
|
||||
patchTree = nil,
|
||||
showingStringDiff = false,
|
||||
oldString = "",
|
||||
newString = "",
|
||||
showingTableDiff = false,
|
||||
oldTable = {},
|
||||
newTable = {},
|
||||
showingSourceDiff = false,
|
||||
oldSource = "",
|
||||
newSource = "",
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
function ConfirmingPage:render()
|
||||
return Theme.with(function(theme)
|
||||
local pageContent = Roact.createFragment({
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
Title = e("TextLabel", {
|
||||
Text = string.format(
|
||||
"Sync changes for project '%s':",
|
||||
self.props.confirmData.serverInfo.projectName or "UNKNOWN"
|
||||
),
|
||||
FontFace = theme.Font.Thin,
|
||||
LayoutOrder = 2,
|
||||
Font = Enum.Font.Gotham,
|
||||
LineHeight = 1.2,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Large + 2),
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
PatchVisualizer = e(PatchVisualizer, {
|
||||
size = UDim2.new(1, 0, 1, -100),
|
||||
size = UDim2.new(1, 0, 1, -150),
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
|
||||
patchTree = self.state.patchTree,
|
||||
changeListHeaders = { "Property", "Current", "Incoming" },
|
||||
patch = self.props.confirmData.patch,
|
||||
instanceMap = self.props.confirmData.instanceMap,
|
||||
|
||||
showStringDiff = function(oldString: string, newString: string)
|
||||
showSourceDiff = function(oldSource: string, newSource: 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,
|
||||
showingSourceDiff = true,
|
||||
oldSource = oldSource,
|
||||
newSource = newSource,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
@@ -148,11 +121,6 @@ function ConfirmingPage:render()
|
||||
}),
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 8),
|
||||
PaddingRight = UDim.new(0, 8),
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
@@ -161,10 +129,15 @@ function ConfirmingPage:render()
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
|
||||
StringDiff = e(StudioPluginGui, {
|
||||
id = "Rojo_ConfirmingStringDiff",
|
||||
title = "String diff",
|
||||
active = self.state.showingStringDiff,
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
|
||||
SourceDiff = e(StudioPluginGui, {
|
||||
id = "Rojo_ConfirmingSourceDiff",
|
||||
title = "Source diff",
|
||||
active = self.state.showingSourceDiff,
|
||||
isEphemeral = true,
|
||||
|
||||
initDockState = Enum.InitialDockState.Float,
|
||||
@@ -176,7 +149,7 @@ function ConfirmingPage:render()
|
||||
|
||||
onClose = function()
|
||||
self:setState({
|
||||
showingStringDiff = false,
|
||||
showingSourceDiff = false,
|
||||
})
|
||||
end,
|
||||
}, {
|
||||
@@ -192,46 +165,8 @@ function ConfirmingPage:render()
|
||||
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,
|
||||
oldText = self.state.oldSource,
|
||||
newText = self.state.newSource,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -3,8 +3,9 @@ local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Flipper = require(Packages.Flipper)
|
||||
|
||||
local timeUtil = require(Plugin.timeUtil)
|
||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local PatchSet = require(Plugin.PatchSet)
|
||||
@@ -17,188 +18,86 @@ local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
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 ChangesViewer = Roact.Component:extend("ChangesViewer")
|
||||
local AGE_UNITS = {
|
||||
{ 31556909, "year" },
|
||||
{ 2629743, "month" },
|
||||
{ 604800, "week" },
|
||||
{ 86400, "day" },
|
||||
{ 3600, "hour" },
|
||||
{
|
||||
60,
|
||||
"minute",
|
||||
},
|
||||
}
|
||||
function timeSinceText(elapsed: number): string
|
||||
if elapsed < 3 then
|
||||
return "just now"
|
||||
end
|
||||
|
||||
function ChangesViewer:init()
|
||||
local ageText = string.format("%d seconds ago", elapsed)
|
||||
|
||||
for _, UnitData in ipairs(AGE_UNITS) do
|
||||
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
|
||||
if elapsed > UnitSeconds then
|
||||
local c = math.floor(elapsed / UnitSeconds)
|
||||
ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return ageText
|
||||
end
|
||||
|
||||
local ChangesDrawer = Roact.Component:extend("ChangesDrawer")
|
||||
|
||||
function ChangesDrawer:init()
|
||||
-- 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
|
||||
|
||||
function ChangesViewer:render()
|
||||
if self.props.rendered == false or self.serveSession == nil or self.props.patchData == nil then
|
||||
function ChangesDrawer:render()
|
||||
if self.props.rendered == false or self.serveSession == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local unapplied = PatchSet.countChanges(self.props.patchData.unapplied)
|
||||
local applied = PatchSet.countChanges(self.props.patchData.patch) - unapplied
|
||||
|
||||
return Theme.with(function(theme)
|
||||
return Roact.createFragment({
|
||||
Navbar = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 40),
|
||||
BackgroundTransparency = 1,
|
||||
return e(BorderedContainer, {
|
||||
transparency = self.props.transparency,
|
||||
size = self.props.height:map(function(y)
|
||||
return UDim2.new(1, 0, y, -220 * y)
|
||||
end),
|
||||
position = UDim2.new(0, 0, 1, 0),
|
||||
anchorPoint = Vector2.new(0, 1),
|
||||
layoutOrder = self.props.layoutOrder,
|
||||
}, {
|
||||
Close = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Close,
|
||||
iconSize = 24,
|
||||
color = theme.ConnectionDetails.DisconnectColor,
|
||||
transparency = self.props.transparency,
|
||||
|
||||
position = UDim2.new(1, 0, 0, 0),
|
||||
anchorPoint = Vector2.new(1, 0),
|
||||
|
||||
onClick = self.props.onClose,
|
||||
}, {
|
||||
Close = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Close,
|
||||
iconSize = 24,
|
||||
color = theme.Settings.Navbar.BackButtonColor,
|
||||
transparency = self.props.transparency,
|
||||
|
||||
position = UDim2.new(0, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(0, 0.5),
|
||||
|
||||
onClick = self.props.onBack,
|
||||
}, {
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Close",
|
||||
}),
|
||||
}),
|
||||
|
||||
Title = e("TextLabel", {
|
||||
Text = "Sync",
|
||||
FontFace = theme.Font.Main,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, -40, 0, theme.TextSize.Large + 2),
|
||||
Position = UDim2.new(0, 40, 0, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
Subtitle = e("TextLabel", {
|
||||
Text = DateTime.fromUnixTimestamp(self.props.patchData.timestamp):FormatLocalTime("LTS", "en-us"),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.SubTextColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
TextTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, -40, 0, theme.TextSize.Medium),
|
||||
Position = UDim2.new(0, 40, 0, theme.TextSize.Large + 2),
|
||||
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,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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),
|
||||
}),
|
||||
}),
|
||||
Tip = e(Tooltip.Trigger, {
|
||||
text = "Close the patch visualizer",
|
||||
}),
|
||||
}),
|
||||
|
||||
Patch = e(PatchVisualizer, {
|
||||
size = UDim2.new(1, -10, 1, -65),
|
||||
position = UDim2.new(0, 5, 1, -5),
|
||||
anchorPoint = Vector2.new(0, 1),
|
||||
PatchVisualizer = e(PatchVisualizer, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = self.props.layoutOrder,
|
||||
layoutOrder = 3,
|
||||
|
||||
patchTree = self.props.patchTree,
|
||||
|
||||
showStringDiff = self.props.showStringDiff,
|
||||
showTableDiff = self.props.showTableDiff,
|
||||
showSourceDiff = self.props.showSourceDiff,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
@@ -217,13 +116,13 @@ local function ConnectionDetails(props)
|
||||
}, {
|
||||
ProjectName = e("TextLabel", {
|
||||
Text = props.projectName,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Large,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 20,
|
||||
TextColor3 = theme.ConnectionDetails.ProjectNameColor,
|
||||
TextTransparency = props.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Large),
|
||||
Size = UDim2.new(1, 0, 0, 20),
|
||||
|
||||
LayoutOrder = 1,
|
||||
BackgroundTransparency = 1,
|
||||
@@ -231,13 +130,13 @@ local function ConnectionDetails(props)
|
||||
|
||||
Address = e("TextLabel", {
|
||||
Text = props.address,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 15,
|
||||
TextColor3 = theme.ConnectionDetails.AddressColor,
|
||||
TextTransparency = props.transparency,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||
Size = UDim2.new(1, 0, 0, 15),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
@@ -266,7 +165,20 @@ function ConnectedPage:getChangeInfoText()
|
||||
if patchData == nil then
|
||||
return ""
|
||||
end
|
||||
return timeUtil.elapsedToText(DateTime.now().UnixTimestamp - patchData.timestamp)
|
||||
|
||||
local elapsed = os.time() - patchData.timestamp
|
||||
local unapplied = PatchSet.countChanges(patchData.unapplied)
|
||||
|
||||
return "<i>Synced "
|
||||
.. timeSinceText(elapsed)
|
||||
.. (if unapplied > 0
|
||||
then string.format(
|
||||
', <font color="#FF8E3C">but %d change%s failed to apply</font>',
|
||||
unapplied,
|
||||
unapplied == 1 and "" or "s"
|
||||
)
|
||||
else "")
|
||||
.. "</i>"
|
||||
end
|
||||
|
||||
function ConnectedPage:startChangeInfoTextUpdater()
|
||||
@@ -276,13 +188,17 @@ function ConnectedPage:startChangeInfoTextUpdater()
|
||||
-- Start a new updater
|
||||
self.changeInfoTextUpdater = task.defer(function()
|
||||
while true do
|
||||
self.setChangeInfoText(self:getChangeInfoText())
|
||||
if self.state.hoveringChangeInfo then
|
||||
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
|
||||
else
|
||||
self.setChangeInfoText(self:getChangeInfoText())
|
||||
end
|
||||
|
||||
local elapsed = DateTime.now().UnixTimestamp - self.props.patchData.timestamp
|
||||
local elapsed = os.time() - self.props.patchData.timestamp
|
||||
local updateInterval = 1
|
||||
|
||||
-- Update timestamp text as frequently as currently needed
|
||||
for _, UnitData in ipairs(timeUtil.AGE_UNITS) do
|
||||
for _, UnitData in ipairs(AGE_UNITS) do
|
||||
local UnitSeconds = UnitData[1]
|
||||
if elapsed > UnitSeconds then
|
||||
updateInterval = UnitSeconds
|
||||
@@ -303,12 +219,29 @@ function ConnectedPage:stopChangeInfoTextUpdater()
|
||||
end
|
||||
|
||||
function ConnectedPage:init()
|
||||
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
|
||||
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
|
||||
|
||||
self.changeDrawerMotor:onStep(function(value)
|
||||
local renderChanges = value > 0.05
|
||||
|
||||
self:setState(function(state)
|
||||
if state.renderChanges == renderChanges then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
renderChanges = renderChanges,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
|
||||
self:setState({
|
||||
renderChanges = false,
|
||||
hoveringChangeInfo = false,
|
||||
showingStringDiff = false,
|
||||
oldString = "",
|
||||
newString = "",
|
||||
showingSourceDiff = false,
|
||||
oldSource = "",
|
||||
newSource = "",
|
||||
})
|
||||
|
||||
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||
@@ -325,16 +258,12 @@ function ConnectedPage:didUpdate(previousProps)
|
||||
-- New patch recieved
|
||||
self:startChangeInfoTextUpdater()
|
||||
self:setState({
|
||||
showingStringDiff = false,
|
||||
showingSourceDiff = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
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 Roact.createFragment({
|
||||
Padding = e("UIPadding", {
|
||||
@@ -349,88 +278,9 @@ function ConnectedPage:render()
|
||||
Padding = UDim.new(0, 10),
|
||||
}),
|
||||
|
||||
Heading = e("Frame", {
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 0, 32),
|
||||
}, {
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
}),
|
||||
|
||||
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,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Body,
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
ConnectionDetails = e(ConnectionDetails, {
|
||||
@@ -480,65 +330,83 @@ function ConnectedPage:render()
|
||||
}),
|
||||
}),
|
||||
|
||||
ChangesViewer = e(StudioPluginGui, {
|
||||
id = "Rojo_ChangesViewer",
|
||||
title = "View changes",
|
||||
active = self.state.renderChanges,
|
||||
isEphemeral = true,
|
||||
ChangeInfo = e("TextButton", {
|
||||
Text = self.changeInfoText,
|
||||
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,
|
||||
|
||||
initDockState = Enum.InitialDockState.Float,
|
||||
overridePreviousState = true,
|
||||
floatingSize = Vector2.new(400, 500),
|
||||
minimumSize = Vector2.new(300, 300),
|
||||
Size = UDim2.new(1, 0, 0, 28),
|
||||
|
||||
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
LayoutOrder = 4,
|
||||
BackgroundTransparency = 1,
|
||||
|
||||
onClose = function()
|
||||
[Roact.Event.MouseEnter] = function()
|
||||
self:setState({
|
||||
renderChanges = false,
|
||||
hoveringChangeInfo = true,
|
||||
})
|
||||
self.setChangeInfoText("<u>" .. self:getChangeInfoText() .. "</u>")
|
||||
end,
|
||||
|
||||
[Roact.Event.MouseLeave] = function()
|
||||
self:setState({
|
||||
hoveringChangeInfo = false,
|
||||
})
|
||||
self.setChangeInfoText(self:getChangeInfoText())
|
||||
end,
|
||||
|
||||
[Roact.Event.Activated] = function()
|
||||
if self.state.renderChanges then
|
||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||
frequency = 4,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
else
|
||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(1, {
|
||||
frequency = 3,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
Tooltip = e(Tooltip.Trigger, {
|
||||
text = if self.state.renderChanges then "Hide the changes" else "View the changes",
|
||||
}),
|
||||
}),
|
||||
|
||||
StringDiff = e(StudioPluginGui, {
|
||||
id = "Rojo_ConnectedStringDiff",
|
||||
title = "String diff",
|
||||
active = self.state.showingStringDiff,
|
||||
ChangesDrawer = e(ChangesDrawer, {
|
||||
rendered = self.state.renderChanges,
|
||||
transparency = self.props.transparency,
|
||||
patchTree = self.props.patchTree,
|
||||
serveSession = self.props.serveSession,
|
||||
height = self.changeDrawerHeight,
|
||||
layoutOrder = 5,
|
||||
|
||||
showSourceDiff = function(oldSource: string, newSource: string)
|
||||
self:setState({
|
||||
showingSourceDiff = true,
|
||||
oldSource = oldSource,
|
||||
newSource = newSource,
|
||||
})
|
||||
end,
|
||||
|
||||
onClose = function()
|
||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||
frequency = 4,
|
||||
dampingRatio = 1,
|
||||
}))
|
||||
end,
|
||||
}),
|
||||
|
||||
SourceDiff = e(StudioPluginGui, {
|
||||
id = "Rojo_ConnectedSourceDiff",
|
||||
title = "Source diff",
|
||||
active = self.state.showingSourceDiff,
|
||||
isEphemeral = true,
|
||||
|
||||
initDockState = Enum.InitialDockState.Float,
|
||||
@@ -550,7 +418,7 @@ function ConnectedPage:render()
|
||||
|
||||
onClose = function()
|
||||
self:setState({
|
||||
showingStringDiff = false,
|
||||
showingSourceDiff = false,
|
||||
})
|
||||
end,
|
||||
}, {
|
||||
@@ -566,46 +434,8 @@ function ConnectedPage:render()
|
||||
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,
|
||||
oldText = self.state.oldSource,
|
||||
newText = self.state.newSource,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -5,10 +7,8 @@ local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
local Header = require(Plugin.App.Components.Header)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||
@@ -24,44 +24,43 @@ function Error:init()
|
||||
end
|
||||
|
||||
function Error:render()
|
||||
return Theme.with(function(theme)
|
||||
return e(BorderedContainer, {
|
||||
size = Roact.joinBindings({
|
||||
containerSize = self.props.containerSize,
|
||||
contentSize = self.contentSize,
|
||||
}):map(function(values)
|
||||
local maximumSize = values.containerSize
|
||||
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
|
||||
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
|
||||
return e(BorderedContainer, {
|
||||
size = Roact.joinBindings({
|
||||
containerSize = self.props.containerSize,
|
||||
contentSize = self.contentSize,
|
||||
}):map(function(values)
|
||||
local maximumSize = values.containerSize
|
||||
maximumSize -= Vector2.new(14, 14) * 2 -- Page padding
|
||||
maximumSize -= Vector2.new(0, 34 + 10) -- Buttons and spacing
|
||||
|
||||
local outerSize = values.contentSize + ERROR_PADDING * 2
|
||||
local outerSize = values.contentSize + ERROR_PADDING * 2
|
||||
|
||||
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
||||
return UDim2.new(1, 0, 0, math.min(outerSize.Y, maximumSize.Y))
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
ScrollingFrame = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize:map(function(value)
|
||||
return value + ERROR_PADDING * 2
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = self.props.layoutOrder,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||
|
||||
local textBounds = TextService:GetTextSize(
|
||||
self.props.errorMessage,
|
||||
16,
|
||||
Enum.Font.Code,
|
||||
Vector2.new(containerSize.X, math.huge)
|
||||
)
|
||||
|
||||
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
||||
end,
|
||||
}, {
|
||||
ScrollingFrame = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize:map(function(value)
|
||||
return value + ERROR_PADDING * 2
|
||||
end),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
[Roact.Change.AbsoluteSize] = function(object)
|
||||
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
|
||||
|
||||
local textBounds = getTextBoundsAsync(
|
||||
self.props.errorMessage,
|
||||
theme.Font.Code,
|
||||
theme.TextSize.Code,
|
||||
containerSize.X
|
||||
)
|
||||
|
||||
self.setContentSize(Vector2.new(containerSize.X, textBounds.Y))
|
||||
end,
|
||||
}, {
|
||||
ErrorMessage = e("TextBox", {
|
||||
ErrorMessage = Theme.with(function(theme)
|
||||
return e("TextBox", {
|
||||
[Roact.Event.InputBegan] = function(rbx, input)
|
||||
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
|
||||
return
|
||||
@@ -72,8 +71,8 @@ function Error:render()
|
||||
|
||||
Text = self.props.errorMessage,
|
||||
TextEditable = false,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 16,
|
||||
TextColor3 = theme.ErrorColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
@@ -82,17 +81,17 @@ function Error:render()
|
||||
ClearTextOnFocus = false,
|
||||
BackgroundTransparency = 1,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
}),
|
||||
})
|
||||
end),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingRight = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
|
||||
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
|
||||
}),
|
||||
Padding = e("UIPadding", {
|
||||
PaddingLeft = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingRight = UDim.new(0, ERROR_PADDING.X),
|
||||
PaddingTop = UDim.new(0, ERROR_PADDING.Y),
|
||||
PaddingBottom = UDim.new(0, ERROR_PADDING.Y),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
local ErrorPage = Roact.Component:extend("ErrorPage")
|
||||
@@ -110,21 +109,16 @@ function ErrorPage:render()
|
||||
self.setContainerSize(object.AbsoluteSize)
|
||||
end,
|
||||
}, {
|
||||
Header = e(Header, {
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
Error = e(Error, {
|
||||
errorMessage = self.state.errorMessage,
|
||||
containerSize = self.containerSize,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 2,
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
Buttons = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 35),
|
||||
LayoutOrder = 3,
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Close = e(TextButton, {
|
||||
|
||||
@@ -27,8 +27,8 @@ local function AddressEntry(props)
|
||||
}, {
|
||||
Host = e("TextBox", {
|
||||
Text = props.host or "",
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Large,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 18,
|
||||
TextColor3 = theme.AddressEntry.TextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = props.transparency,
|
||||
@@ -51,8 +51,8 @@ local function AddressEntry(props)
|
||||
|
||||
Port = e("TextBox", {
|
||||
Text = props.port or "",
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Large,
|
||||
Font = Enum.Font.Code,
|
||||
TextSize = 18,
|
||||
TextColor3 = theme.AddressEntry.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
PlaceholderText = Config.defaultPort,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -7,55 +9,24 @@ local Roact = require(Packages.Roact)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local Checkbox = require(Plugin.App.Components.Checkbox)
|
||||
local Dropdown = require(Plugin.App.Components.Dropdown)
|
||||
local IconButton = require(Plugin.App.Components.IconButton)
|
||||
local Tag = require(Plugin.App.Components.Tag)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
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 getTextBoundsWithLineHeight(
|
||||
text: string,
|
||||
font: Font,
|
||||
textSize: number,
|
||||
width: number,
|
||||
lineHeight: number
|
||||
)
|
||||
local textBounds = getTextBoundsAsync(text, font, textSize, width)
|
||||
local function getTextBounds(text, textSize, font, lineHeight, bounds)
|
||||
local textBounds = TextService:GetTextSize(text, textSize, font, bounds)
|
||||
|
||||
local lineCount = math.ceil(textBounds.Y / textSize)
|
||||
local lineCount = textBounds.Y / textSize
|
||||
local lineHeightAbsolute = textSize * lineHeight
|
||||
|
||||
return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize))
|
||||
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")
|
||||
|
||||
function Setting:init()
|
||||
@@ -80,11 +51,11 @@ end
|
||||
|
||||
function Setting:render()
|
||||
return Theme.with(function(theme)
|
||||
local settingsTheme = theme.Settings
|
||||
theme = theme.Settings
|
||||
|
||||
return e("Frame", {
|
||||
Size = self.contentSize:map(function(value)
|
||||
return UDim2.new(1, 0, 0, value.Y + 20)
|
||||
return UDim2.new(1, 0, 0, 20 + value.Y + 20)
|
||||
end),
|
||||
LayoutOrder = self.props.layoutOrder,
|
||||
ZIndex = -self.props.layoutOrder,
|
||||
@@ -114,7 +85,6 @@ function Setting:render()
|
||||
then self.props.input
|
||||
elseif self.props.options ~= nil then e(Dropdown, {
|
||||
locked = self.props.locked,
|
||||
lockedTooltip = self.props.lockedTooltip,
|
||||
options = self.props.options,
|
||||
active = self.state.setting,
|
||||
transparency = self.props.transparency,
|
||||
@@ -124,7 +94,6 @@ function Setting:render()
|
||||
})
|
||||
else e(Checkbox, {
|
||||
locked = self.props.locked,
|
||||
lockedTooltip = self.props.lockedTooltip,
|
||||
active = self.state.setting,
|
||||
transparency = self.props.transparency,
|
||||
onClick = function()
|
||||
@@ -137,7 +106,7 @@ function Setting:render()
|
||||
then e(IconButton, {
|
||||
icon = Assets.Images.Icons.Reset,
|
||||
iconSize = 24,
|
||||
color = settingsTheme.BackButtonColor,
|
||||
color = theme.BackButtonColor,
|
||||
transparency = self.props.transparency,
|
||||
visible = self.props.showReset,
|
||||
layoutOrder = -1,
|
||||
@@ -151,49 +120,29 @@ function Setting:render()
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Heading = e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||
Name = e("TextLabel", {
|
||||
Text = (if self.props.experimental then '<font color="#FF8E3C">⚠ </font>' else "")
|
||||
.. self.props.name,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 17,
|
||||
TextColor3 = theme.Setting.NameColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
RichText = true,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, 17),
|
||||
|
||||
LayoutOrder = 1,
|
||||
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", {
|
||||
Text = self.props.name,
|
||||
FontFace = theme.Font.Bold,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
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,
|
||||
TextTransparency = self.props.transparency,
|
||||
RichText = true,
|
||||
|
||||
Size = UDim2.new(1, 0, 0, theme.TextSize.Medium),
|
||||
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
}),
|
||||
|
||||
Description = e("TextLabel", {
|
||||
Text = self.props.description,
|
||||
FontFace = theme.Font.Main,
|
||||
Text = (if self.props.experimental then '<font color="#FF8E3C">[Experimental] </font>' else "")
|
||||
.. self.props.description,
|
||||
Font = Enum.Font.Gotham,
|
||||
LineHeight = 1.2,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = settingsTheme.Setting.DescriptionColor,
|
||||
TextSize = 14,
|
||||
TextColor3 = theme.Setting.DescriptionColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextTransparency = self.props.transparency,
|
||||
TextWrapped = true,
|
||||
@@ -203,18 +152,20 @@ function Setting:render()
|
||||
containerSize = self.containerSize,
|
||||
inputSize = self.inputSize,
|
||||
}):map(function(values)
|
||||
local desc = (if self.props.experimental then "[Experimental] " else "")
|
||||
.. self.props.description
|
||||
local offset = values.inputSize.X + 5
|
||||
local textBounds = getTextBoundsWithLineHeight(
|
||||
self.props.description,
|
||||
theme.Font.Main,
|
||||
theme.TextSize.Body,
|
||||
values.containerSize.X - offset,
|
||||
1.2
|
||||
local textBounds = getTextBounds(
|
||||
desc,
|
||||
14,
|
||||
Enum.Font.Gotham,
|
||||
1.2,
|
||||
Vector2.new(values.containerSize.X - offset, math.huge)
|
||||
)
|
||||
return UDim2.new(1, -offset, 0, textBounds.Y)
|
||||
end),
|
||||
|
||||
LayoutOrder = 3,
|
||||
LayoutOrder = 2,
|
||||
BackgroundTransparency = 1,
|
||||
}),
|
||||
|
||||
@@ -222,16 +173,21 @@ function Setting:render()
|
||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||
FillDirection = Enum.FillDirection.Vertical,
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
Padding = UDim.new(0, 5),
|
||||
Padding = UDim.new(0, 6),
|
||||
|
||||
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||
self.setContentSize(object.AbsoluteContentSize)
|
||||
end,
|
||||
}),
|
||||
|
||||
Padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 20),
|
||||
PaddingBottom = UDim.new(0, 20),
|
||||
}),
|
||||
}),
|
||||
|
||||
Divider = e("Frame", {
|
||||
BackgroundColor3 = settingsTheme.DividerColor,
|
||||
BackgroundColor3 = theme.DividerColor,
|
||||
BackgroundTransparency = self.props.transparency,
|
||||
Size = UDim2.new(1, 0, 0, 1),
|
||||
BorderSizePixel = 0,
|
||||
|
||||
@@ -27,11 +27,10 @@ end
|
||||
|
||||
local invertedLevels = invertTbl(Log.Level)
|
||||
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
|
||||
local syncReminderModes = { "None", "Notify", "Fullscreen" }
|
||||
|
||||
local function Navbar(props)
|
||||
return Theme.with(function(theme)
|
||||
local navbarTheme = theme.Settings.Navbar
|
||||
theme = theme.Settings.Navbar
|
||||
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 46),
|
||||
@@ -41,7 +40,7 @@ local function Navbar(props)
|
||||
Back = e(IconButton, {
|
||||
icon = Assets.Images.Icons.Back,
|
||||
iconSize = 24,
|
||||
color = navbarTheme.BackButtonColor,
|
||||
color = theme.BackButtonColor,
|
||||
transparency = props.transparency,
|
||||
|
||||
position = UDim2.new(0, 0, 0.5, 0),
|
||||
@@ -56,9 +55,9 @@ local function Navbar(props)
|
||||
|
||||
Text = e("TextLabel", {
|
||||
Text = "Settings",
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Large,
|
||||
TextColor3 = navbarTheme.TextColor,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextSize = 18,
|
||||
TextColor3 = theme.TextColor,
|
||||
TextTransparency = props.transparency,
|
||||
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
@@ -76,30 +75,18 @@ function SettingsPage:init()
|
||||
end
|
||||
|
||||
function SettingsPage:render()
|
||||
local layoutOrder = 0
|
||||
local function layoutIncrement()
|
||||
layoutOrder += 1
|
||||
return layoutOrder
|
||||
end
|
||||
return Theme.with(function(theme)
|
||||
theme = theme.Settings
|
||||
|
||||
return Roact.createFragment({
|
||||
Navbar = e(Navbar, {
|
||||
onBack = self.props.onBack,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
Content = e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, -47),
|
||||
position = UDim2.new(0, 0, 0, 47),
|
||||
return e(ScrollingFrame, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
contentSize = self.contentSize,
|
||||
transparency = self.props.transparency,
|
||||
}, {
|
||||
AutoReconnect = e(Setting, {
|
||||
id = "autoReconnect",
|
||||
name = "Auto Reconnect",
|
||||
description = "Reconnect to server on place open if the served project matches the last sync to the place",
|
||||
Navbar = e(Navbar, {
|
||||
onBack = self.props.onBack,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 0,
|
||||
}),
|
||||
|
||||
ShowNotifications = e(Setting, {
|
||||
@@ -107,29 +94,16 @@ function SettingsPage:render()
|
||||
name = "Show Notifications",
|
||||
description = "Popup notifications in viewport",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 1,
|
||||
}),
|
||||
|
||||
SyncReminderMode = e(Setting, {
|
||||
id = "syncReminderMode",
|
||||
SyncReminder = e(Setting, {
|
||||
id = "syncReminder",
|
||||
name = "Sync Reminder",
|
||||
description = "What type of reminders you receive for syncing your project",
|
||||
description = "Notify to sync when opening a place that has previously been synced",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = Settings:getBinding("showNotifications"),
|
||||
|
||||
options = syncReminderModes,
|
||||
}),
|
||||
|
||||
SyncReminderPolling = e(Setting, {
|
||||
id = "syncReminderPolling",
|
||||
name = "Sync Reminder Polling",
|
||||
description = "Look for available sync servers periodically",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = Settings:getBindings("syncReminderMode", "showNotifications"):map(function(values)
|
||||
return values.syncReminderMode ~= "None" and values.showNotifications
|
||||
end),
|
||||
layoutOrder = 2,
|
||||
}),
|
||||
|
||||
ConfirmationBehavior = e(Setting, {
|
||||
@@ -137,7 +111,7 @@ function SettingsPage:render()
|
||||
name = "Confirmation Behavior",
|
||||
description = "When to prompt for confirmation before syncing",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 3,
|
||||
|
||||
options = confirmationBehaviors,
|
||||
}),
|
||||
@@ -147,7 +121,7 @@ function SettingsPage:render()
|
||||
name = "Confirmation Threshold",
|
||||
description = "How many modified instances to be considered a large change",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 4,
|
||||
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
|
||||
return value == "Large Changes"
|
||||
end),
|
||||
@@ -178,52 +152,17 @@ function SettingsPage:render()
|
||||
name = "Play Sounds",
|
||||
description = "Toggle sound effects",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
EnableSyncFallback = e(Setting, {
|
||||
id = "enableSyncFallback",
|
||||
name = "Enable Sync Fallback",
|
||||
description = "Whether Instances that fail to sync are remade as a fallback. If this is enabled, Instances may be destroyed and remade when syncing.",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
CheckForUpdates = e(Setting, {
|
||||
id = "checkForUpdates",
|
||||
name = "Check For Updates",
|
||||
description = "Notify about newer compatible Rojo releases",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
CheckForPreleases = e(Setting, {
|
||||
id = "checkForPrereleases",
|
||||
name = "Include Prerelease Updates",
|
||||
description = "Include prereleases when checking for updates",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil
|
||||
then false -- Must be a local install to allow prerelease checks
|
||||
else Settings:getBinding("checkForUpdates"),
|
||||
}),
|
||||
|
||||
AutoConnectPlaytestServer = e(Setting, {
|
||||
id = "autoConnectPlaytestServer",
|
||||
name = "Auto Connect Playtest Server",
|
||||
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
|
||||
tag = "unstable",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 5,
|
||||
}),
|
||||
|
||||
OpenScriptsExternally = e(Setting, {
|
||||
id = "openScriptsExternally",
|
||||
name = "Open Scripts Externally",
|
||||
description = "Attempt to open scripts in an external editor",
|
||||
tag = "unstable",
|
||||
locked = self.props.syncActive,
|
||||
experimental = true,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 6,
|
||||
}),
|
||||
|
||||
TwoWaySync = e(Setting, {
|
||||
@@ -231,19 +170,17 @@ function SettingsPage:render()
|
||||
name = "Two-Way Sync",
|
||||
description = "Editing files in Studio will sync them into the filesystem",
|
||||
locked = self.props.syncActive,
|
||||
lockedTooltip = "(Cannot change while currently syncing. Disconnect first.)",
|
||||
tag = "unstable",
|
||||
experimental = true,
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 7,
|
||||
}),
|
||||
|
||||
LogLevel = e(Setting, {
|
||||
id = "logLevel",
|
||||
name = "Log Level",
|
||||
description = "Plugin output verbosity level",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 100,
|
||||
|
||||
options = invertedLevels,
|
||||
showReset = Settings:getBinding("logLevel"):map(function(value)
|
||||
@@ -258,18 +195,8 @@ function SettingsPage:render()
|
||||
id = "typecheckingEnabled",
|
||||
name = "Typechecking",
|
||||
description = "Toggle typechecking on the API surface",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
}),
|
||||
|
||||
TimingLogsEnabled = e(Setting, {
|
||||
id = "timingLogsEnabled",
|
||||
name = "Timing Logs",
|
||||
description = "Toggle logging timing of internal actions for benchmarking Rojo performance",
|
||||
tag = "debug",
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = layoutIncrement(),
|
||||
layoutOrder = 101,
|
||||
}),
|
||||
|
||||
Layout = e("UIListLayout", {
|
||||
@@ -285,8 +212,8 @@ function SettingsPage:render()
|
||||
PaddingLeft = UDim.new(0, 20),
|
||||
PaddingRight = UDim.new(0, 20),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return SettingsPage
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
--[[
|
||||
Theming system provided through Roact's context.
|
||||
Uses Studio colors when possible.
|
||||
Theming system taking advantage of Roact's new context API.
|
||||
Doesn't use colors provided by Studio and instead just branches on theme
|
||||
name. This isn't exactly best practice.
|
||||
]]
|
||||
|
||||
-- Studio does not exist outside Roblox Studio, so we'll lazily initialize it
|
||||
@@ -14,8 +15,6 @@ local function getStudio()
|
||||
return _Studio
|
||||
end
|
||||
|
||||
local ContentProvider = game:GetService("ContentProvider")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
@@ -25,7 +24,227 @@ local strict = require(script.Parent.Parent.strict)
|
||||
|
||||
local BRAND_COLOR = Color3.fromHex("E13835")
|
||||
|
||||
local Context = Roact.createContext({})
|
||||
local lightTheme = strict("LightTheme", {
|
||||
BackgroundColor = Color3.fromHex("FFFFFF"),
|
||||
Button = {
|
||||
Solid = {
|
||||
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 = Color3.fromHex("000000"),
|
||||
ActionFillTransparency = 0.9,
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("393939"),
|
||||
BorderColor = Color3.fromHex("ACACAC"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("393939"),
|
||||
BorderColor = Color3.fromHex("ACACAC"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Checkbox = {
|
||||
Active = {
|
||||
IconColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = BRAND_COLOR,
|
||||
},
|
||||
Inactive = {
|
||||
IconColor = Color3.fromHex("EEEEEE"),
|
||||
BorderColor = Color3.fromHex("AFAFAF"),
|
||||
},
|
||||
},
|
||||
Dropdown = {
|
||||
TextColor = Color3.fromHex("000000"),
|
||||
BorderColor = Color3.fromHex("AFAFAF"),
|
||||
BackgroundColor = Color3.fromHex("EEEEEE"),
|
||||
Open = {
|
||||
IconColor = BRAND_COLOR,
|
||||
},
|
||||
Closed = {
|
||||
IconColor = Color3.fromHex("EEEEEE"),
|
||||
},
|
||||
},
|
||||
TextInput = {
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("000000"),
|
||||
PlaceholderColor = Color3.fromHex("8C8C8C"),
|
||||
BorderColor = Color3.fromHex("ACACAC"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("393939"),
|
||||
PlaceholderColor = Color3.fromHex("8C8C8C"),
|
||||
BorderColor = Color3.fromHex("AFAFAF"),
|
||||
},
|
||||
ActionFillColor = Color3.fromHex("000000"),
|
||||
ActionFillTransparency = 0.9,
|
||||
},
|
||||
AddressEntry = {
|
||||
TextColor = Color3.fromHex("000000"),
|
||||
PlaceholderColor = Color3.fromHex("8C8C8C"),
|
||||
},
|
||||
BorderedContainer = {
|
||||
BorderColor = Color3.fromHex("CBCBCB"),
|
||||
BackgroundColor = Color3.fromHex("EEEEEE"),
|
||||
},
|
||||
Spinner = {
|
||||
ForegroundColor = BRAND_COLOR,
|
||||
BackgroundColor = Color3.fromHex("EEEEEE"),
|
||||
},
|
||||
Diff = {
|
||||
Add = Color3.fromHex("baffbd"),
|
||||
Remove = Color3.fromHex("ffbdba"),
|
||||
Edit = Color3.fromHex("bacdff"),
|
||||
Row = Color3.fromHex("000000"),
|
||||
Warning = Color3.fromHex("FF8E3C"),
|
||||
},
|
||||
ConnectionDetails = {
|
||||
ProjectNameColor = Color3.fromHex("000000"),
|
||||
AddressColor = Color3.fromHex("000000"),
|
||||
DisconnectColor = BRAND_COLOR,
|
||||
},
|
||||
Settings = {
|
||||
DividerColor = Color3.fromHex("CBCBCB"),
|
||||
Navbar = {
|
||||
BackButtonColor = Color3.fromHex("000000"),
|
||||
TextColor = Color3.fromHex("000000"),
|
||||
},
|
||||
Setting = {
|
||||
NameColor = Color3.fromHex("000000"),
|
||||
DescriptionColor = Color3.fromHex("5F5F5F"),
|
||||
},
|
||||
},
|
||||
Header = {
|
||||
LogoColor = BRAND_COLOR,
|
||||
VersionColor = Color3.fromHex("727272"),
|
||||
},
|
||||
Notification = {
|
||||
InfoColor = Color3.fromHex("000000"),
|
||||
CloseColor = BRAND_COLOR,
|
||||
},
|
||||
ErrorColor = Color3.fromHex("000000"),
|
||||
ScrollBarColor = Color3.fromHex("000000"),
|
||||
})
|
||||
|
||||
local darkTheme = strict("DarkTheme", {
|
||||
BackgroundColor = Color3.fromHex("2E2E2E"),
|
||||
Button = {
|
||||
Solid = {
|
||||
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 = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.9,
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("DBDBDB"),
|
||||
BorderColor = Color3.fromHex("535353"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("DBDBDB"),
|
||||
BorderColor = Color3.fromHex("535353"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Checkbox = {
|
||||
Active = {
|
||||
IconColor = Color3.fromHex("FFFFFF"),
|
||||
BackgroundColor = BRAND_COLOR,
|
||||
},
|
||||
Inactive = {
|
||||
IconColor = Color3.fromHex("484848"),
|
||||
BorderColor = Color3.fromHex("5A5A5A"),
|
||||
},
|
||||
},
|
||||
Dropdown = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
BorderColor = Color3.fromHex("5A5A5A"),
|
||||
BackgroundColor = Color3.fromHex("2B2B2B"),
|
||||
Open = {
|
||||
IconColor = BRAND_COLOR,
|
||||
},
|
||||
Closed = {
|
||||
IconColor = Color3.fromHex("484848"),
|
||||
},
|
||||
},
|
||||
TextInput = {
|
||||
Enabled = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
PlaceholderColor = Color3.fromHex("8B8B8B"),
|
||||
BorderColor = Color3.fromHex("535353"),
|
||||
},
|
||||
Disabled = {
|
||||
TextColor = Color3.fromHex("484848"),
|
||||
PlaceholderColor = Color3.fromHex("8B8B8B"),
|
||||
BorderColor = Color3.fromHex("5A5A5A"),
|
||||
},
|
||||
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||
ActionFillTransparency = 0.9,
|
||||
},
|
||||
AddressEntry = {
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
PlaceholderColor = Color3.fromHex("8B8B8B"),
|
||||
},
|
||||
BorderedContainer = {
|
||||
BorderColor = Color3.fromHex("535353"),
|
||||
BackgroundColor = Color3.fromHex("2B2B2B"),
|
||||
},
|
||||
Spinner = {
|
||||
ForegroundColor = BRAND_COLOR,
|
||||
BackgroundColor = Color3.fromHex("2B2B2B"),
|
||||
},
|
||||
Diff = {
|
||||
Add = Color3.fromHex("273732"),
|
||||
Remove = Color3.fromHex("3F2D32"),
|
||||
Edit = Color3.fromHex("193345"),
|
||||
Row = Color3.fromHex("FFFFFF"),
|
||||
Warning = Color3.fromHex("FF8E3C"),
|
||||
},
|
||||
ConnectionDetails = {
|
||||
ProjectNameColor = Color3.fromHex("FFFFFF"),
|
||||
AddressColor = Color3.fromHex("FFFFFF"),
|
||||
DisconnectColor = Color3.fromHex("FFFFFF"),
|
||||
},
|
||||
Settings = {
|
||||
DividerColor = Color3.fromHex("535353"),
|
||||
Navbar = {
|
||||
BackButtonColor = Color3.fromHex("FFFFFF"),
|
||||
TextColor = Color3.fromHex("FFFFFF"),
|
||||
},
|
||||
Setting = {
|
||||
NameColor = Color3.fromHex("FFFFFF"),
|
||||
DescriptionColor = Color3.fromHex("D3D3D3"),
|
||||
},
|
||||
},
|
||||
Header = {
|
||||
LogoColor = BRAND_COLOR,
|
||||
VersionColor = Color3.fromHex("D3D3D3"),
|
||||
},
|
||||
Notification = {
|
||||
InfoColor = Color3.fromHex("FFFFFF"),
|
||||
CloseColor = Color3.fromHex("FFFFFF"),
|
||||
},
|
||||
ErrorColor = Color3.fromHex("FFFFFF"),
|
||||
ScrollBarColor = Color3.fromHex("FFFFFF"),
|
||||
})
|
||||
|
||||
local Context = Roact.createContext(lightTheme)
|
||||
|
||||
local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||
|
||||
@@ -33,209 +252,25 @@ local StudioProvider = Roact.Component:extend("StudioProvider")
|
||||
function StudioProvider:updateTheme()
|
||||
local studioTheme = getStudio().Theme
|
||||
|
||||
local isDark = studioTheme.Name == "Dark"
|
||||
if studioTheme.Name == "Light" then
|
||||
self:setState({
|
||||
theme = lightTheme,
|
||||
})
|
||||
elseif studioTheme.Name == "Dark" then
|
||||
self:setState({
|
||||
theme = darkTheme,
|
||||
})
|
||||
else
|
||||
Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
|
||||
|
||||
local theme = strict(studioTheme.Name .. "Theme", {
|
||||
Font = {
|
||||
Main = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Medium, Enum.FontStyle.Normal),
|
||||
Bold = Font.new("rbxasset://fonts/families/Montserrat.json", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
|
||||
Thin = Font.new(
|
||||
"rbxasset://fonts/families/Montserrat.json",
|
||||
Enum.FontWeight.Regular,
|
||||
Enum.FontStyle.Normal
|
||||
),
|
||||
Code = Font.new(
|
||||
"rbxasset://fonts/families/Inconsolata.json",
|
||||
Enum.FontWeight.Regular,
|
||||
Enum.FontStyle.Normal
|
||||
),
|
||||
},
|
||||
TextSize = {
|
||||
Body = 15,
|
||||
Small = 13,
|
||||
Medium = 16,
|
||||
Large = 18,
|
||||
Code = 16,
|
||||
},
|
||||
BrandColor = BRAND_COLOR,
|
||||
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 = {
|
||||
-- Very bright different colors in case some places were not updated to use
|
||||
-- the new background diff colors.
|
||||
Add = Color3.fromRGB(255, 0, 255),
|
||||
Remove = Color3.fromRGB(255, 0, 255),
|
||||
Edit = Color3.fromRGB(255, 0, 255),
|
||||
|
||||
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
|
||||
|
||||
Background = {
|
||||
-- Studio doesn't have good colors since their diffs use backgrounds, not text
|
||||
Add = if isDark then Color3.fromRGB(143, 227, 154) else Color3.fromRGB(41, 164, 45),
|
||||
Remove = if isDark then Color3.fromRGB(242, 125, 125) else Color3.fromRGB(150, 29, 29),
|
||||
Edit = if isDark then Color3.fromRGB(120, 154, 248) else Color3.fromRGB(0, 70, 160),
|
||||
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
|
||||
},
|
||||
|
||||
Text = {
|
||||
Add = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||
Remove = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||
Edit = if isDark then Color3.new(0, 0, 0) else Color3.new(1, 1, 1),
|
||||
Remain = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
|
||||
},
|
||||
},
|
||||
ConnectionDetails = {
|
||||
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),
|
||||
})
|
||||
|
||||
self:setState({
|
||||
theme = theme,
|
||||
})
|
||||
self:setState({
|
||||
theme = lightTheme,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function StudioProvider:init()
|
||||
self:updateTheme()
|
||||
|
||||
-- Preload the Fonts so that getTextBoundsAsync won't yield
|
||||
local fontAssetIds = {}
|
||||
for _, font in self.state.theme.Font do
|
||||
table.insert(fontAssetIds, font.Family)
|
||||
end
|
||||
pcall(ContentProvider.PreloadAsync, ContentProvider, fontAssetIds)
|
||||
end
|
||||
|
||||
function StudioProvider:render()
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local params = Instance.new("GetTextBoundsParams")
|
||||
|
||||
local function getTextBoundsAsync(
|
||||
text: string,
|
||||
font: Font,
|
||||
textSize: number,
|
||||
width: number,
|
||||
richText: boolean?
|
||||
): Vector2
|
||||
if type(text) ~= "string" then
|
||||
Log.warn(`Invalid text. Expected string, received {type(text)} instead`)
|
||||
return Vector2.zero
|
||||
end
|
||||
if #text >= 200_000 then
|
||||
Log.warn(`Invalid text. Exceeds the 199,999 character limit`)
|
||||
return Vector2.zero
|
||||
end
|
||||
|
||||
params.Text = text
|
||||
params.Font = font
|
||||
params.Size = textSize
|
||||
params.Width = width
|
||||
params.RichText = not not richText
|
||||
|
||||
local success, bounds = pcall(TextService.GetTextBoundsAsync, TextService, params)
|
||||
if not success then
|
||||
Log.warn(`Failed to get text bounds: {bounds}`)
|
||||
return Vector2.zero
|
||||
end
|
||||
|
||||
return bounds
|
||||
end
|
||||
|
||||
return getTextBoundsAsync
|
||||
@@ -9,7 +9,6 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
local Promise = require(Packages.Promise)
|
||||
|
||||
local Assets = require(Plugin.Assets)
|
||||
local Version = require(Plugin.Version)
|
||||
@@ -24,11 +23,10 @@ local PatchTree = require(Plugin.PatchTree)
|
||||
local preloadAssets = require(Plugin.preloadAssets)
|
||||
local soundPlayer = require(Plugin.soundPlayer)
|
||||
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
||||
local timeUtil = require(Plugin.timeUtil)
|
||||
local Theme = require(script.Theme)
|
||||
|
||||
local Page = require(script.Page)
|
||||
local Notifications = require(script.Components.Notifications)
|
||||
local Notifications = require(script.Notifications)
|
||||
local Tooltip = require(script.Components.Tooltip)
|
||||
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
||||
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
||||
@@ -53,9 +51,9 @@ local App = Roact.Component:extend("App")
|
||||
function App:init()
|
||||
preloadAssets()
|
||||
|
||||
local priorSyncInfo = self:getPriorSyncInfo()
|
||||
self.host, self.setHost = Roact.createBinding(priorSyncInfo.host or "")
|
||||
self.port, self.setPort = Roact.createBinding(priorSyncInfo.port or "")
|
||||
local priorHost, priorPort = self:getPriorEndpoint()
|
||||
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
||||
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
||||
|
||||
self.confirmationBindable = Instance.new("BindableEvent")
|
||||
self.confirmationEvent = self.confirmationBindable.Event
|
||||
@@ -79,18 +77,17 @@ function App:init()
|
||||
action
|
||||
)
|
||||
)
|
||||
local dismissNotif = self:addNotification({
|
||||
text = string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
||||
timeout = 10,
|
||||
onClose = function()
|
||||
cleanup()
|
||||
end,
|
||||
actions = {
|
||||
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()
|
||||
onClick = function(notification)
|
||||
cleanup()
|
||||
notification:dismiss()
|
||||
ChangeHistoryService:Redo()
|
||||
end,
|
||||
},
|
||||
@@ -98,9 +95,13 @@ function App:init()
|
||||
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
|
||||
@@ -117,13 +118,6 @@ function App:init()
|
||||
end)
|
||||
end)
|
||||
|
||||
self.disconnectUpdatesCheckChanged = Settings:onChanged("checkForUpdates", function()
|
||||
self:checkForUpdates()
|
||||
end)
|
||||
self.disconnectPrereleasesCheckChanged = Settings:onChanged("checkForPrereleases", function()
|
||||
self:checkForUpdates()
|
||||
end)
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
guiEnabled = false,
|
||||
@@ -137,65 +131,45 @@ function App:init()
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
|
||||
if RunService:IsEdit() then
|
||||
self:checkForUpdates()
|
||||
|
||||
self:startSyncReminderPolling()
|
||||
self.disconnectSyncReminderPollingChanged = Settings:onChanged("syncReminderPolling", function(enabled)
|
||||
if enabled then
|
||||
self:startSyncReminderPolling()
|
||||
else
|
||||
self:stopSyncReminderPolling()
|
||||
end
|
||||
end)
|
||||
|
||||
self:tryAutoReconnect():andThen(function(didReconnect)
|
||||
if not didReconnect then
|
||||
self:checkSyncReminder()
|
||||
end
|
||||
end)
|
||||
if
|
||||
RunService:IsEdit()
|
||||
and self.serveSession == nil
|
||||
and Settings:get("syncReminder")
|
||||
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
|
||||
|
||||
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()
|
||||
if self.disconnectSyncReminderPollingChanged then
|
||||
self.disconnectSyncReminderPollingChanged()
|
||||
end
|
||||
|
||||
self:stopSyncReminderPolling()
|
||||
|
||||
self.autoConnectPlaytestServerListener()
|
||||
self:clearRunningConnectionInfo()
|
||||
end
|
||||
|
||||
function App:addNotification(notif: {
|
||||
function App:addNotification(
|
||||
text: string,
|
||||
isFullscreen: boolean?,
|
||||
timeout: number?,
|
||||
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> ()? } }?,
|
||||
onClose: (any) -> ()?,
|
||||
})
|
||||
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
|
||||
)
|
||||
if not Settings:get("showNotifications") then
|
||||
return
|
||||
end
|
||||
@@ -203,17 +177,17 @@ function App:addNotification(notif: {
|
||||
self.notifId += 1
|
||||
local id = self.notifId
|
||||
|
||||
self:setState(function(prevState)
|
||||
local notifications = table.clone(prevState.notifications)
|
||||
notifications[id] = Dictionary.merge({
|
||||
timeout = notif.timeout or 5,
|
||||
isFullscreen = notif.isFullscreen or false,
|
||||
}, notif)
|
||||
local notifications = table.clone(self.state.notifications)
|
||||
notifications[id] = {
|
||||
text = text,
|
||||
timestamp = DateTime.now().UnixTimestampMillis,
|
||||
timeout = timeout or 3,
|
||||
actions = actions,
|
||||
}
|
||||
|
||||
return {
|
||||
notifications = notifications,
|
||||
}
|
||||
end)
|
||||
self:setState({
|
||||
notifications = notifications,
|
||||
})
|
||||
|
||||
return function()
|
||||
self:closeNotification(id)
|
||||
@@ -225,60 +199,62 @@ function App:closeNotification(id: number)
|
||||
return
|
||||
end
|
||||
|
||||
self:setState(function(prevState)
|
||||
local notifications = table.clone(prevState.notifications)
|
||||
notifications[id] = nil
|
||||
local notifications = table.clone(self.state.notifications)
|
||||
notifications[id] = nil
|
||||
|
||||
return {
|
||||
notifications = notifications,
|
||||
}
|
||||
end)
|
||||
self:setState({
|
||||
notifications = notifications,
|
||||
})
|
||||
end
|
||||
|
||||
function App:checkForUpdates()
|
||||
local updateMessage = Version.getUpdateMessage()
|
||||
|
||||
if updateMessage then
|
||||
self:addNotification({
|
||||
text = updateMessage,
|
||||
timeout = 500,
|
||||
actions = {
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function App:getPriorSyncInfo(): { host: string?, port: string?, projectName: string?, timestamp: number? }
|
||||
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||
if not priorSyncInfos then
|
||||
return {}
|
||||
function App:getPriorEndpoint()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
return
|
||||
end
|
||||
|
||||
local id = tostring(game.PlaceId)
|
||||
if ignorePlaceIds[id] then
|
||||
return {}
|
||||
return
|
||||
end
|
||||
|
||||
return priorSyncInfos[id] or {}
|
||||
local place = priorEndpoints[id]
|
||||
if not place then
|
||||
return
|
||||
end
|
||||
|
||||
return place.host, place.port
|
||||
end
|
||||
|
||||
function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
||||
local priorSyncInfos = Settings:get("priorEndpoints")
|
||||
if not priorSyncInfos then
|
||||
priorSyncInfos = {}
|
||||
function App:getLastSyncTimestamp()
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
return
|
||||
end
|
||||
|
||||
local now = os.time()
|
||||
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)
|
||||
local priorEndpoints = Settings:get("priorEndpoints")
|
||||
if not priorEndpoints then
|
||||
priorEndpoints = {}
|
||||
end
|
||||
|
||||
-- Clear any stale saves to avoid disc bloat
|
||||
for placeId, syncInfo in priorSyncInfos do
|
||||
if now - (syncInfo.timestamp or now) > 12_960_000 then
|
||||
priorSyncInfos[placeId] = nil
|
||||
for placeId, endpoint in priorEndpoints do
|
||||
if os.time() - endpoint.timestamp > 12_960_000 then
|
||||
priorEndpoints[placeId] = nil
|
||||
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
||||
end
|
||||
end
|
||||
@@ -288,22 +264,24 @@ function App:setPriorSyncInfo(host: string, port: string, projectName: string)
|
||||
return
|
||||
end
|
||||
|
||||
priorSyncInfos[id] = {
|
||||
priorEndpoints[id] = {
|
||||
host = if host ~= Config.defaultHost then host else nil,
|
||||
port = if port ~= Config.defaultPort then port else nil,
|
||||
projectName = projectName,
|
||||
timestamp = now,
|
||||
timestamp = os.time(),
|
||||
}
|
||||
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
||||
|
||||
Settings:set("priorEndpoints", priorSyncInfos)
|
||||
Settings:set("priorEndpoints", priorEndpoints)
|
||||
end
|
||||
|
||||
function App:getHostAndPort()
|
||||
local host = self.host:getValue()
|
||||
local port = self.port:getValue()
|
||||
|
||||
return if #host > 0 then host else Config.defaultHost, if #port > 0 then port else Config.defaultPort
|
||||
local host = if #host > 0 then host else Config.defaultHost
|
||||
local port = if #port > 0 then port else Config.defaultPort
|
||||
|
||||
return host, port
|
||||
end
|
||||
|
||||
function App:isSyncLockAvailable()
|
||||
@@ -371,209 +349,13 @@ function App:releaseSyncLock()
|
||||
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
||||
end
|
||||
|
||||
function App:findActiveServer()
|
||||
local host, port = self:getHostAndPort()
|
||||
local baseUrl = if string.find(host, "^https?://")
|
||||
then string.format("%s:%s", host, port)
|
||||
else string.format("http://%s:%s", host, port)
|
||||
|
||||
Log.trace("Checking for active sync server at {}", baseUrl)
|
||||
|
||||
local apiContext = ApiContext.new(baseUrl)
|
||||
return apiContext:connect():andThen(function(serverInfo)
|
||||
apiContext:disconnect()
|
||||
return serverInfo, host, port
|
||||
end)
|
||||
end
|
||||
|
||||
function App:tryAutoReconnect()
|
||||
if not Settings:get("autoReconnect") then
|
||||
return Promise.resolve(false)
|
||||
end
|
||||
|
||||
local priorSyncInfo = self:getPriorSyncInfo()
|
||||
if not priorSyncInfo.projectName then
|
||||
Log.trace("No prior sync info found, skipping auto-reconnect")
|
||||
return Promise.resolve(false)
|
||||
end
|
||||
|
||||
return self:findActiveServer()
|
||||
:andThen(function(serverInfo)
|
||||
-- change
|
||||
if serverInfo.projectName == priorSyncInfo.projectName then
|
||||
Log.trace("Auto-reconnect found matching server, reconnecting...")
|
||||
self:addNotification({
|
||||
text = `Auto-reconnect discovered project '{serverInfo.projectName}'...`,
|
||||
})
|
||||
self:startSession()
|
||||
return true
|
||||
end
|
||||
Log.trace("Auto-reconnect found different server, not reconnecting")
|
||||
return false
|
||||
end)
|
||||
:catch(function()
|
||||
Log.trace("Auto-reconnect did not find a server, not reconnecting")
|
||||
return false
|
||||
end)
|
||||
end
|
||||
|
||||
function App:checkSyncReminder()
|
||||
local syncReminderMode = Settings:get("syncReminderMode")
|
||||
if syncReminderMode == "None" then
|
||||
return
|
||||
end
|
||||
|
||||
if self.serveSession ~= nil or not self:isSyncLockAvailable() then
|
||||
-- Already syncing or cannot sync, no reason to remind
|
||||
return
|
||||
end
|
||||
|
||||
local priorSyncInfo = self:getPriorSyncInfo()
|
||||
|
||||
self:findActiveServer()
|
||||
:andThen(function(serverInfo, host, port)
|
||||
self:sendSyncReminder(
|
||||
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
|
||||
)
|
||||
end)
|
||||
:catch(function()
|
||||
if priorSyncInfo.timestamp and priorSyncInfo.projectName then
|
||||
-- We didn't find an active server,
|
||||
-- but this place has a prior sync
|
||||
-- so we should remind the user to serve
|
||||
|
||||
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
|
||||
self:sendSyncReminder(
|
||||
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function App:startSyncReminderPolling()
|
||||
if
|
||||
self.syncReminderPollingThread ~= nil
|
||||
or Settings:get("syncReminderMode") == "None"
|
||||
or not Settings:get("syncReminderPolling")
|
||||
then
|
||||
return
|
||||
end
|
||||
|
||||
Log.trace("Starting sync reminder polling thread")
|
||||
self.syncReminderPollingThread = task.spawn(function()
|
||||
while task.wait(30) do
|
||||
if self.syncReminderPollingThread == nil then
|
||||
-- The polling thread was stopped, so exit
|
||||
return
|
||||
end
|
||||
if self.dismissSyncReminder then
|
||||
-- There is already a sync reminder being shown
|
||||
task.wait(5)
|
||||
continue
|
||||
end
|
||||
self:checkSyncReminder()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function App:stopSyncReminderPolling()
|
||||
if self.syncReminderPollingThread then
|
||||
Log.trace("Stopping sync reminder polling thread")
|
||||
task.cancel(self.syncReminderPollingThread)
|
||||
self.syncReminderPollingThread = nil
|
||||
end
|
||||
end
|
||||
|
||||
function App:sendSyncReminder(message: string)
|
||||
local syncReminderMode = Settings:get("syncReminderMode")
|
||||
if syncReminderMode == "None" then
|
||||
return
|
||||
end
|
||||
|
||||
self.dismissSyncReminder = self:addNotification({
|
||||
text = message,
|
||||
timeout = 120,
|
||||
isFullscreen = Settings:get("syncReminderMode") == "Fullscreen",
|
||||
onClose = function()
|
||||
self.dismissSyncReminder = nil
|
||||
end,
|
||||
actions = {
|
||||
Connect = {
|
||||
text = "Connect",
|
||||
style = "Solid",
|
||||
layoutOrder = 1,
|
||||
onClick = function()
|
||||
self:startSession()
|
||||
end,
|
||||
},
|
||||
Dismiss = {
|
||||
text = "Dismiss",
|
||||
style = "Bordered",
|
||||
layoutOrder = 2,
|
||||
onClick = function()
|
||||
-- If the user dismisses the reminder,
|
||||
-- then we don't need to remind them again
|
||||
self:stopSyncReminderPolling()
|
||||
end,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
function App:isAutoConnectPlaytestServerAvailable()
|
||||
return RunService:IsRunning()
|
||||
and RunService:IsStudio()
|
||||
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()
|
||||
local claimedLock, priorOwner = self:claimSyncLock()
|
||||
if not claimedLock then
|
||||
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
||||
|
||||
Log.warn(msg)
|
||||
self:addNotification({
|
||||
text = msg,
|
||||
timeout = 10,
|
||||
})
|
||||
self:addNotification(msg, 10)
|
||||
self:setState({
|
||||
appStatus = AppStatus.Error,
|
||||
errorMessage = msg,
|
||||
@@ -585,6 +367,11 @@ function App:startSession()
|
||||
|
||||
local host, port = self:getHostAndPort()
|
||||
|
||||
local sessionOptions = {
|
||||
openScriptsExternally = Settings:get("openScriptsExternally"),
|
||||
twoWaySync = Settings:get("twoWaySync"),
|
||||
}
|
||||
|
||||
local baseUrl = if string.find(host, "^https?://")
|
||||
then string.format("%s:%s", host, port)
|
||||
else string.format("http://%s:%s", host, port)
|
||||
@@ -592,16 +379,17 @@ function App:startSession()
|
||||
|
||||
local serveSession = ServeSession.new({
|
||||
apiContext = apiContext,
|
||||
twoWaySync = Settings:get("twoWaySync"),
|
||||
openScriptsExternally = sessionOptions.openScriptsExternally,
|
||||
twoWaySync = sessionOptions.twoWaySync,
|
||||
})
|
||||
|
||||
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
|
||||
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:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||
-- Update tree with unapplied metadata
|
||||
self:setState(function(prevState)
|
||||
return {
|
||||
@@ -611,7 +399,7 @@ function App:startSession()
|
||||
end)
|
||||
|
||||
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
|
||||
local now = DateTime.now().UnixTimestamp
|
||||
local now = os.time()
|
||||
local old = self.state.patchData
|
||||
|
||||
if PatchSet.isEmpty(patch) then
|
||||
@@ -644,22 +432,15 @@ function App:startSession()
|
||||
|
||||
serveSession:onStatusChanged(function(status, details)
|
||||
if status == ServeSession.Status.Connecting then
|
||||
if self.dismissSyncReminder then
|
||||
self.dismissSyncReminder()
|
||||
self.dismissSyncReminder = nil
|
||||
end
|
||||
self:setPriorEndpoint(host, port)
|
||||
|
||||
self:setState({
|
||||
appStatus = AppStatus.Connecting,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
self:addNotification({
|
||||
text = "Connecting to session...",
|
||||
})
|
||||
self:addNotification("Connecting to session...")
|
||||
elseif status == ServeSession.Status.Connected then
|
||||
self.knownProjects[details] = true
|
||||
self:setPriorSyncInfo(host, port, details)
|
||||
self:setRunningConnectionInfo(baseUrl)
|
||||
|
||||
local address = ("%s:%s"):format(host, port)
|
||||
self:setState({
|
||||
@@ -668,13 +449,10 @@ function App:startSession()
|
||||
address = address,
|
||||
toolbarIcon = Assets.Images.PluginButtonConnected,
|
||||
})
|
||||
self:addNotification({
|
||||
text = string.format("Connected to session '%s' at %s.", details, address),
|
||||
})
|
||||
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
||||
elseif status == ServeSession.Status.Disconnected then
|
||||
self.serveSession = nil
|
||||
self:releaseSyncLock()
|
||||
self:clearRunningConnectionInfo()
|
||||
self:setState({
|
||||
patchData = {
|
||||
patch = PatchSet.newEmpty(),
|
||||
@@ -693,19 +471,13 @@ function App:startSession()
|
||||
errorMessage = tostring(details),
|
||||
toolbarIcon = Assets.Images.PluginButtonWarning,
|
||||
})
|
||||
self:addNotification({
|
||||
text = tostring(details),
|
||||
timeout = 10,
|
||||
})
|
||||
self:addNotification(tostring(details), 10)
|
||||
else
|
||||
self:setState({
|
||||
appStatus = AppStatus.NotConnected,
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
self:addNotification({
|
||||
text = "Disconnected from session.",
|
||||
timeout = 10,
|
||||
})
|
||||
self:addNotification("Disconnected from session.")
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -716,12 +488,6 @@ function App:startSession()
|
||||
return "Accept"
|
||||
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
|
||||
@@ -783,13 +549,13 @@ function App:startSession()
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
})
|
||||
|
||||
self:addNotification({
|
||||
text = string.format(
|
||||
self:addNotification(
|
||||
string.format(
|
||||
"Please accept%sor abort the initializing sync session.",
|
||||
Settings:get("twoWaySync") and ", reject, " or " "
|
||||
),
|
||||
timeout = 7,
|
||||
})
|
||||
7
|
||||
)
|
||||
|
||||
return self.confirmationEvent:Wait()
|
||||
end)
|
||||
@@ -840,7 +606,7 @@ function App:render()
|
||||
value = self.props.plugin,
|
||||
}, {
|
||||
e(Theme.StudioProvider, nil, {
|
||||
tooltip = e(Tooltip.Provider, nil, {
|
||||
e(Tooltip.Provider, nil, {
|
||||
gui = e(StudioPluginGui, {
|
||||
id = pluginName,
|
||||
title = pluginName,
|
||||
@@ -950,7 +716,19 @@ function App:render()
|
||||
ResetOnSpawn = false,
|
||||
DisplayOrder = 100,
|
||||
}, {
|
||||
Notifications = e(Notifications, {
|
||||
layout = e("UIListLayout", {
|
||||
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
||||
Padding = UDim.new(0, 5),
|
||||
}),
|
||||
padding = e("UIPadding", {
|
||||
PaddingTop = UDim.new(0, 5),
|
||||
PaddingBottom = UDim.new(0, 5),
|
||||
PaddingLeft = UDim.new(0, 5),
|
||||
PaddingRight = UDim.new(0, 5),
|
||||
}),
|
||||
notifs = e(Notifications, {
|
||||
soundPlayer = self.props.soundPlayer,
|
||||
notifications = self.state.notifications,
|
||||
onClose = function(id)
|
||||
|
||||
@@ -25,12 +25,6 @@ local Assets = {
|
||||
Back = "rbxassetid://6017213752",
|
||||
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 = {
|
||||
Add = "rbxassetid://10434145835",
|
||||
|
||||
@@ -112,12 +112,9 @@ end
|
||||
|
||||
function InstanceMap:destroyInstance(instance)
|
||||
local id = self.fromInstances[instance]
|
||||
local descendants = instance:GetDescendants()
|
||||
|
||||
-- Because the user might want to Undo this change, we cannot use Destroy
|
||||
-- since that locks that parent and prevents ChangeHistoryService from
|
||||
-- ever bringing it back. Instead, we parent to nil.
|
||||
instance.Parent = nil
|
||||
local descendants = instance:GetDescendants()
|
||||
instance:Destroy()
|
||||
|
||||
-- After the instance is successfully destroyed,
|
||||
-- we can remove all the id mappings
|
||||
|
||||
@@ -211,11 +211,9 @@ end
|
||||
function PatchSet.countChanges(patch)
|
||||
local count = 0
|
||||
|
||||
for _, add in patch.added do
|
||||
-- Adding an instance is 1 change per property
|
||||
for _ in add.Properties do
|
||||
count += 1
|
||||
end
|
||||
for _ in patch.added do
|
||||
-- Adding an instance is 1 change
|
||||
count += 1
|
||||
end
|
||||
for _ in patch.removed do
|
||||
-- Removing an instance is 1 change
|
||||
@@ -282,22 +280,6 @@ function PatchSet.assign(target, ...)
|
||||
return target
|
||||
end
|
||||
|
||||
function PatchSet.addedIdList(patchSet): { string }
|
||||
local idList = table.create(#patchSet.added)
|
||||
for id in patchSet.added do
|
||||
table.insert(idList, id)
|
||||
end
|
||||
return table.freeze(idList)
|
||||
end
|
||||
|
||||
function PatchSet.updatedIdList(patchSet): { string }
|
||||
local idList = table.create(#patchSet.updated)
|
||||
for _, item in patchSet.updated do
|
||||
table.insert(idList, item.id)
|
||||
end
|
||||
return table.freeze(idList)
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a list of human-readable statements summarizing the contents of this
|
||||
patch, intended to be displayed to users.
|
||||
|
||||
@@ -11,7 +11,6 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local Types = require(Plugin.Types)
|
||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
||||
@@ -79,15 +78,6 @@ function Tree.new()
|
||||
return setmetatable(tree, Tree)
|
||||
end
|
||||
|
||||
-- Iterates over all nodes and counts them up
|
||||
function Tree:getCount()
|
||||
local count = 0
|
||||
self:forEach(function()
|
||||
count += 1
|
||||
end)
|
||||
return count
|
||||
end
|
||||
|
||||
-- Iterates over all sub-nodes, depth first
|
||||
-- node is where to start from, defaults to root
|
||||
-- depth is used for recursion but can be used to set the starting depth
|
||||
@@ -132,7 +122,6 @@ end
|
||||
-- props must contain id, and cannot contain children or parentId
|
||||
-- other than those three, it can hold anything
|
||||
function Tree:addNode(parent, props)
|
||||
Timer.start("Tree:addNode")
|
||||
assert(props.id, "props must contain id")
|
||||
|
||||
parent = parent or "ROOT"
|
||||
@@ -143,7 +132,6 @@ function Tree:addNode(parent, props)
|
||||
for k, v in props do
|
||||
node[k] = v
|
||||
end
|
||||
Timer.stop()
|
||||
return node
|
||||
end
|
||||
|
||||
@@ -154,21 +142,18 @@ function Tree:addNode(parent, props)
|
||||
local parentNode = self:getNode(parent)
|
||||
if not parentNode then
|
||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
||||
Timer.stop()
|
||||
return
|
||||
end
|
||||
|
||||
parentNode.children[node.id] = node
|
||||
self.idToNode[node.id] = node
|
||||
|
||||
Timer.stop()
|
||||
return node
|
||||
end
|
||||
|
||||
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
||||
-- using the patch and instanceMap info
|
||||
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
|
||||
Timer.start("Tree:buildAncestryNodes")
|
||||
-- Build nodes for ancestry by going up the tree
|
||||
previousId = previousId or "ROOT"
|
||||
|
||||
@@ -186,8 +171,6 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
|
||||
})
|
||||
previousId = ancestorId
|
||||
end
|
||||
|
||||
Timer.stop()
|
||||
end
|
||||
|
||||
local PatchTree = {}
|
||||
@@ -195,12 +178,10 @@ local PatchTree = {}
|
||||
-- Builds a new tree from a patch and instanceMap
|
||||
-- uses changeListHeaders in node.changeList
|
||||
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
Timer.start("PatchTree.build")
|
||||
local tree = Tree.new()
|
||||
|
||||
local knownAncestors = {}
|
||||
|
||||
Timer.start("patch.updated")
|
||||
for _, change in patch.updated do
|
||||
local instance = instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
@@ -228,14 +209,15 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
|
||||
|
||||
-- Gather detail text
|
||||
local changeList, changeInfo = nil, nil
|
||||
local changeList, hint = nil, nil
|
||||
if next(change.changedProperties) or change.changedName then
|
||||
changeList = {}
|
||||
|
||||
local changeIndex = 0
|
||||
local hintBuffer, i = {}, 0
|
||||
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
|
||||
changeIndex += 1
|
||||
changeList[changeIndex] = { prop, current, incoming, metadata }
|
||||
i += 1
|
||||
hintBuffer[i] = prop
|
||||
changeList[i] = { prop, current, incoming, metadata }
|
||||
end
|
||||
|
||||
-- Gather the changes
|
||||
@@ -255,9 +237,19 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
)
|
||||
end
|
||||
|
||||
changeInfo = {
|
||||
edits = changeIndex,
|
||||
}
|
||||
-- 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)
|
||||
@@ -273,13 +265,11 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
className = instance.ClassName,
|
||||
name = instance.Name,
|
||||
instance = instance,
|
||||
changeInfo = changeInfo,
|
||||
hint = hint,
|
||||
changeList = changeList,
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
Timer.start("patch.removed")
|
||||
for _, idOrInstance in patch.removed do
|
||||
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
|
||||
if not instance then
|
||||
@@ -321,9 +311,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
instance = instance,
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
Timer.start("patch.added")
|
||||
for id, change in patch.added do
|
||||
-- Gather ancestors from existing DOM or future additions
|
||||
local ancestryIds = {}
|
||||
@@ -358,24 +346,36 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
|
||||
|
||||
-- Gather detail text
|
||||
local changeList, changeInfo = nil, nil
|
||||
local changeList, hint = nil, nil
|
||||
if next(change.Properties) then
|
||||
changeList = {}
|
||||
|
||||
local changeIndex = 0
|
||||
local function addProp(prop: string, incoming: any)
|
||||
changeIndex += 1
|
||||
changeList[changeIndex] = { prop, "N/A", incoming }
|
||||
end
|
||||
|
||||
local hintBuffer, i = {}, 0
|
||||
for prop, incoming in change.Properties do
|
||||
i += 1
|
||||
hintBuffer[i] = prop
|
||||
|
||||
local success, incomingValue = decodeValue(incoming, instanceMap)
|
||||
addProp(prop, if success then incomingValue else select(2, next(incoming)))
|
||||
if success then
|
||||
table.insert(changeList, { prop, "N/A", incomingValue })
|
||||
else
|
||||
table.insert(changeList, { prop, "N/A", select(2, next(incoming)) })
|
||||
end
|
||||
end
|
||||
|
||||
changeInfo = {
|
||||
edits = changeIndex,
|
||||
}
|
||||
-- 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)
|
||||
@@ -390,32 +390,40 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
patchType = "Add",
|
||||
className = change.ClassName,
|
||||
name = change.Name,
|
||||
changeInfo = changeInfo,
|
||||
hint = hint,
|
||||
changeList = changeList,
|
||||
instance = instanceMap.fromIds[id],
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
Timer.stop()
|
||||
return tree
|
||||
end
|
||||
|
||||
-- Creates a deep copy of a tree for immutability purposes in Roact
|
||||
function PatchTree.clone(tree)
|
||||
if not tree then
|
||||
return
|
||||
end
|
||||
|
||||
local newTree = Tree.new()
|
||||
tree:forEach(function(node)
|
||||
newTree:addNode(node.parentId, table.clone(node))
|
||||
end)
|
||||
|
||||
return newTree
|
||||
end
|
||||
|
||||
-- Updates the metadata of a tree with the unapplied patch and currently existing instances
|
||||
-- Builds a new tree from the data if one isn't provided
|
||||
-- Always returns a new tree for immutability purposes in Roact
|
||||
function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
Timer.start("PatchTree.updateMetadata")
|
||||
if tree then
|
||||
-- A shallow copy is enough for our purposes here since we really only need a new top-level object
|
||||
-- for immutable comparison checks in Roact
|
||||
tree = table.clone(tree)
|
||||
tree = PatchTree.clone(tree)
|
||||
else
|
||||
tree = PatchTree.build(patch, instanceMap)
|
||||
end
|
||||
|
||||
-- Update isWarning metadata
|
||||
Timer.start("isWarning")
|
||||
for _, failedChange in unappliedPatch.updated do
|
||||
local node = tree:getNode(failedChange.id)
|
||||
if not node then
|
||||
@@ -428,8 +436,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
if not node.changeList then
|
||||
continue
|
||||
end
|
||||
|
||||
local warnings = 0
|
||||
for _, change in node.changeList do
|
||||
local property = change[1]
|
||||
local propertyFailedToApply = if property == "Name"
|
||||
@@ -440,8 +446,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
-- This change didn't fail, no need to mark
|
||||
continue
|
||||
end
|
||||
|
||||
warnings += 1
|
||||
if change[4] == nil then
|
||||
change[4] = { isWarning = true }
|
||||
else
|
||||
@@ -449,11 +453,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
end
|
||||
Log.trace(" Marked property as warning: {}.{}", node.name, property)
|
||||
end
|
||||
|
||||
node.changeInfo = {
|
||||
edits = (node.changeInfo.edits or (#node.changeList - 1)) - warnings,
|
||||
failed = if warnings > 0 then warnings else nil,
|
||||
}
|
||||
end
|
||||
for failedAdditionId in unappliedPatch.added do
|
||||
local node = tree:getNode(failedAdditionId)
|
||||
@@ -467,7 +466,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
if not node.changeList then
|
||||
continue
|
||||
end
|
||||
|
||||
for _, change in node.changeList do
|
||||
-- Failed addition means that all properties failed to be added
|
||||
if change[4] == nil then
|
||||
@@ -477,10 +475,6 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
end
|
||||
Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
|
||||
end
|
||||
|
||||
node.changeInfo = {
|
||||
failed = node.changeInfo.edits or (#node.changeList - 1),
|
||||
}
|
||||
end
|
||||
for _, failedRemovalIdOrInstance in unappliedPatch.removed do
|
||||
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)
|
||||
@@ -498,10 +492,8 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
node.isWarning = true
|
||||
Log.trace("Marked node as warning: {} {}", node.id, node.name)
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
-- Update if instances exist
|
||||
Timer.start("instanceAncestry")
|
||||
tree:forEach(function(node)
|
||||
if node.instance then
|
||||
if node.instance.Parent == nil and node.instance ~= game then
|
||||
@@ -517,9 +509,7 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
|
||||
end
|
||||
end
|
||||
end)
|
||||
Timer.stop()
|
||||
|
||||
Timer.stop()
|
||||
return tree
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
Patches can come from the server or be generated by the client.
|
||||
]]
|
||||
|
||||
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||
|
||||
local Packages = script.Parent.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
@@ -14,18 +16,14 @@ local invariant = require(script.Parent.Parent.invariant)
|
||||
|
||||
local decodeValue = require(script.Parent.decodeValue)
|
||||
local reify = require(script.Parent.reify)
|
||||
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||
local setProperty = require(script.Parent.setProperty)
|
||||
|
||||
local function applyPatch(instanceMap, patch)
|
||||
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
|
||||
|
||||
-- Tracks any portions of the patch that could not be applied to the DOM.
|
||||
local unappliedPatch = PatchSet.newEmpty()
|
||||
|
||||
-- Contains a list of all of the ref properties that we'll need to assign.
|
||||
-- It is imperative that refs are assigned after all instances are created
|
||||
-- to ensure that referents can be mapped to instances correctly.
|
||||
local deferredRefs = {}
|
||||
|
||||
for _, removedIdOrInstance in ipairs(patch.removed) do
|
||||
local removeInstanceSuccess = pcall(function()
|
||||
if Types.RbxId(removedIdOrInstance) then
|
||||
@@ -72,7 +70,7 @@ local function applyPatch(instanceMap, patch)
|
||||
)
|
||||
end
|
||||
|
||||
local failedToReify = reifyInstance(deferredRefs, instanceMap, patch.added, id, parentInstance)
|
||||
local failedToReify = reify(instanceMap, patch.added, id, parentInstance)
|
||||
|
||||
if not PatchSet.isEmpty(failedToReify) then
|
||||
Log.debug("Failed to reify as part of applying a patch: {:#?}", failedToReify)
|
||||
@@ -137,7 +135,7 @@ local function applyPatch(instanceMap, patch)
|
||||
[update.id] = mockVirtualInstance,
|
||||
}
|
||||
|
||||
local failedToReify = reifyInstance(deferredRefs, instanceMap, mockAdded, update.id, instance.Parent)
|
||||
local failedToReify = reify(instanceMap, mockAdded, update.id, instance.Parent)
|
||||
|
||||
local newInstance = instanceMap.fromIds[update.id]
|
||||
|
||||
@@ -166,14 +164,10 @@ local function applyPatch(instanceMap, patch)
|
||||
end
|
||||
|
||||
-- See you later, original instance.
|
||||
|
||||
-- Because the user might want to Undo this change, we cannot use Destroy
|
||||
-- since that locks that parent and prevents ChangeHistoryService from
|
||||
-- ever bringing it back. Instead, we parent to nil.
|
||||
|
||||
--
|
||||
-- TODO: Can this fail? Some kinds of instance may not appreciate
|
||||
-- being reparented, like services.
|
||||
instance.Parent = nil
|
||||
-- being destroyed, like services.
|
||||
instance:Destroy()
|
||||
|
||||
-- This completes your rebuilding a plane mid-flight safety
|
||||
-- instruction. Please sit back, relax, and enjoy your flight.
|
||||
@@ -200,18 +194,6 @@ local function applyPatch(instanceMap, patch)
|
||||
|
||||
if update.changedProperties ~= nil then
|
||||
for propertyName, propertyValue in pairs(update.changedProperties) do
|
||||
-- Because refs may refer to instances that we haven't constructed yet,
|
||||
-- we defer applying any ref properties until all instances are created.
|
||||
if next(propertyValue) == "Ref" then
|
||||
table.insert(deferredRefs, {
|
||||
id = update.id,
|
||||
instance = instance,
|
||||
propertyName = propertyName,
|
||||
virtualValue = propertyValue,
|
||||
})
|
||||
continue
|
||||
end
|
||||
|
||||
local decodeSuccess, decodedValue = decodeValue(propertyValue, instanceMap)
|
||||
if not decodeSuccess then
|
||||
unappliedUpdate.changedProperties[propertyName] = propertyValue
|
||||
@@ -232,7 +214,7 @@ local function applyPatch(instanceMap, patch)
|
||||
end
|
||||
end
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
ChangeHistoryService:SetWaypoint("Rojo: Patch " .. patchTimestamp)
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
@@ -4,41 +4,25 @@ return function()
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
|
||||
local container = Instance.new("Folder")
|
||||
|
||||
local tempContainer = Instance.new("Folder")
|
||||
local function wasRemoved(instance)
|
||||
local dummy = Instance.new("Folder")
|
||||
local function wasDestroyed(instance)
|
||||
-- If an instance was destroyed, its parent property is locked.
|
||||
-- If an instance was removed, its parent property is nil.
|
||||
-- We need to ensure we only remove, so that ChangeHistoryService can still Undo.
|
||||
|
||||
local isParentUnlocked = pcall(function()
|
||||
local ok = pcall(function()
|
||||
local oldParent = instance.Parent
|
||||
instance.Parent = tempContainer
|
||||
instance.Parent = dummy
|
||||
instance.Parent = oldParent
|
||||
end)
|
||||
|
||||
return instance.Parent == nil and isParentUnlocked
|
||||
return not ok
|
||||
end
|
||||
|
||||
beforeEach(function()
|
||||
container:ClearAllChildren()
|
||||
end)
|
||||
|
||||
afterAll(function()
|
||||
container:Destroy()
|
||||
tempContainer:Destroy()
|
||||
end)
|
||||
|
||||
it("should return an empty patch if given an empty patch", function()
|
||||
local patch = applyPatch(InstanceMap.new(), PatchSet.newEmpty())
|
||||
assert(PatchSet.isEmpty(patch), "expected remaining patch to be empty")
|
||||
end)
|
||||
|
||||
it("should remove instances listed for remove", function()
|
||||
it("should destroy instances listed for remove", function()
|
||||
local root = Instance.new("Folder")
|
||||
root.Name = "ROOT"
|
||||
root.Parent = container
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Name = "Child"
|
||||
@@ -54,16 +38,14 @@ return function()
|
||||
local unapplied = applyPatch(instanceMap, patch)
|
||||
assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty")
|
||||
|
||||
assert(not wasRemoved(root), "expected root to be left alone")
|
||||
assert(wasRemoved(child), "expected child to be removed")
|
||||
assert(not wasDestroyed(root), "expected root to be left alone")
|
||||
assert(wasDestroyed(child), "expected child to be destroyed")
|
||||
|
||||
instanceMap:stop()
|
||||
end)
|
||||
|
||||
it("should remove IDs listed for remove", function()
|
||||
it("should destroy IDs listed for remove", function()
|
||||
local root = Instance.new("Folder")
|
||||
root.Name = "ROOT"
|
||||
root.Parent = container
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Name = "Child"
|
||||
@@ -80,8 +62,8 @@ return function()
|
||||
assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty")
|
||||
expect(instanceMap:size()).to.equal(1)
|
||||
|
||||
assert(not wasRemoved(root), "expected root to be left alone")
|
||||
assert(wasRemoved(child), "expected child to be removed")
|
||||
assert(not wasDestroyed(root), "expected root to be left alone")
|
||||
assert(wasDestroyed(child), "expected child to be destroyed")
|
||||
|
||||
instanceMap:stop()
|
||||
end)
|
||||
@@ -91,8 +73,6 @@ return function()
|
||||
-- tests on reify, not here.
|
||||
|
||||
local root = Instance.new("Folder")
|
||||
root.Name = "ROOT"
|
||||
root.Parent = container
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
instanceMap:insert("ROOT", root)
|
||||
@@ -133,8 +113,6 @@ return function()
|
||||
|
||||
it("should return unapplied additions when instances cannot be created", function()
|
||||
local root = Instance.new("Folder")
|
||||
root.Name = "ROOT"
|
||||
root.Parent = container
|
||||
|
||||
local instanceMap = InstanceMap.new()
|
||||
instanceMap:insert("ROOT", root)
|
||||
@@ -181,7 +159,6 @@ return function()
|
||||
it("should recreate instances when changedClassName is set, preserving children", function()
|
||||
local root = Instance.new("Folder")
|
||||
root.Name = "Initial Root Name"
|
||||
root.Parent = container
|
||||
|
||||
local child = Instance.new("Folder")
|
||||
child.Name = "Child"
|
||||
|
||||
@@ -25,14 +25,6 @@ local function trueEquals(a, b): boolean
|
||||
return true
|
||||
end
|
||||
|
||||
-- Treat nil and { Ref = "000...0" } as equal
|
||||
if
|
||||
(a == nil and type(b) == "table" and b.Ref == "00000000000000000000000000000000")
|
||||
or (b == nil and type(a) == "table" and a.Ref == "00000000000000000000000000000000")
|
||||
then
|
||||
return true
|
||||
end
|
||||
|
||||
local typeA, typeB = typeof(a), typeof(b)
|
||||
|
||||
-- For tables, try recursive deep equality
|
||||
@@ -159,24 +151,7 @@ local function diff(instanceMap, virtualInstances, rootId)
|
||||
|
||||
if getProperySuccess then
|
||||
local existingValue = existingValueOrErr
|
||||
local decodeSuccess, decodedValue
|
||||
|
||||
-- If `virtualValue` is a ref then instead of decoding it to an instance,
|
||||
-- we change `existingValue` to be a ref. This is because `virtualValue`
|
||||
-- may point to an Instance which doesn't exist yet and therefore
|
||||
-- decoding it may throw an error.
|
||||
if next(virtualValue) == "Ref" then
|
||||
decodeSuccess, decodedValue = true, virtualValue
|
||||
|
||||
if existingValue and typeof(existingValue) == "Instance" then
|
||||
local existingValueRef = instanceMap.fromInstances[existingValue]
|
||||
if existingValueRef then
|
||||
existingValue = { Ref = existingValueRef }
|
||||
end
|
||||
end
|
||||
else
|
||||
decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||
end
|
||||
local decodeSuccess, decodedValue = decodeValue(virtualValue, instanceMap)
|
||||
|
||||
if decodeSuccess then
|
||||
if not trueEquals(existingValue, decodedValue) then
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
and mutating the Roblox DOM.
|
||||
]]
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
|
||||
local applyPatch = require(script.applyPatch)
|
||||
local hydrate = require(script.hydrate)
|
||||
@@ -19,34 +17,71 @@ function Reconciler.new(instanceMap)
|
||||
local self = {
|
||||
-- Tracks all of the instances known by the reconciler by ID.
|
||||
__instanceMap = instanceMap,
|
||||
__precommitCallbacks = {},
|
||||
__postcommitCallbacks = {},
|
||||
}
|
||||
|
||||
return setmetatable(self, Reconciler)
|
||||
end
|
||||
|
||||
function Reconciler:hookPrecommit(callback: (patch: any, instanceMap: any) -> ()): () -> ()
|
||||
table.insert(self.__precommitCallbacks, callback)
|
||||
Log.trace("Added precommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__precommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__precommitCallbacks, i)
|
||||
Log.trace("Removed precommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unappliedPatch: any) -> ()): () -> ()
|
||||
table.insert(self.__postcommitCallbacks, callback)
|
||||
Log.trace("Added postcommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__postcommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__postcommitCallbacks, i)
|
||||
Log.trace("Removed postcommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Reconciler:applyPatch(patch)
|
||||
Timer.start("Reconciler:applyPatch")
|
||||
for _, callback in self.__precommitCallbacks do
|
||||
local success, err = pcall(callback, patch, self.__instanceMap)
|
||||
if not success then
|
||||
Log.warn("Precommit hook errored: {}", err)
|
||||
end
|
||||
end
|
||||
|
||||
local unappliedPatch = applyPatch(self.__instanceMap, patch)
|
||||
|
||||
Timer.stop()
|
||||
for _, callback in self.__postcommitCallbacks do
|
||||
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
|
||||
if not success then
|
||||
Log.warn("Postcommit hook errored: {}", err)
|
||||
end
|
||||
end
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
|
||||
Timer.start("Reconciler:hydrate")
|
||||
local result = hydrate(self.__instanceMap, virtualInstances, rootId, rootInstance)
|
||||
Timer.stop()
|
||||
|
||||
return result
|
||||
return hydrate(self.__instanceMap, virtualInstances, rootId, rootInstance)
|
||||
end
|
||||
|
||||
function Reconciler:diff(virtualInstances, rootId)
|
||||
Timer.start("Reconciler:diff")
|
||||
local success, result = diff(self.__instanceMap, virtualInstances, rootId)
|
||||
Timer.stop()
|
||||
|
||||
return success, result
|
||||
return diff(self.__instanceMap, virtualInstances, rootId)
|
||||
end
|
||||
|
||||
return Reconciler
|
||||
|
||||
@@ -7,6 +7,26 @@ local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
local setProperty = require(script.Parent.setProperty)
|
||||
local decodeValue = require(script.Parent.decodeValue)
|
||||
|
||||
local reifyInner, applyDeferredRefs
|
||||
|
||||
local function reify(instanceMap, virtualInstances, rootId, parentInstance)
|
||||
-- Create an empty patch that will be populated with any parts of this reify
|
||||
-- that could not happen, like instances that couldn't be created and
|
||||
-- properties that could not be assigned.
|
||||
local unappliedPatch = PatchSet.newEmpty()
|
||||
|
||||
-- Contains a list of all of the ref properties that we'll need to assign
|
||||
-- after all instances are created. We apply refs in a second pass, after
|
||||
-- we create as many instances as we can, so that we ensure that referents
|
||||
-- can be mapped to instances correctly.
|
||||
local deferredRefs = {}
|
||||
|
||||
reifyInner(instanceMap, virtualInstances, rootId, parentInstance, unappliedPatch, deferredRefs)
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
--[[
|
||||
Add the given ID and all of its descendants in virtualInstances to the given
|
||||
PatchSet, marked for addition.
|
||||
@@ -20,21 +40,10 @@ local function addAllToPatch(patchSet, virtualInstances, id)
|
||||
end
|
||||
end
|
||||
|
||||
function reifyInstance(deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
||||
-- Create an empty patch that will be populated with any parts of this reify
|
||||
-- that could not happen, like instances that couldn't be created and
|
||||
-- properties that could not be assigned.
|
||||
local unappliedPatch = PatchSet.newEmpty()
|
||||
|
||||
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, rootId, parentInstance)
|
||||
|
||||
return unappliedPatch
|
||||
end
|
||||
|
||||
--[[
|
||||
Inner function that defines the core routine.
|
||||
]]
|
||||
function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, id, parentInstance)
|
||||
function reifyInner(instanceMap, virtualInstances, id, parentInstance, unappliedPatch, deferredRefs)
|
||||
local virtualInstance = virtualInstances[id]
|
||||
|
||||
if virtualInstance == nil then
|
||||
@@ -93,7 +102,7 @@ function reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualIn
|
||||
end
|
||||
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
reifyInstanceInner(unappliedPatch, deferredRefs, instanceMap, virtualInstances, childId, instance)
|
||||
reifyInner(instanceMap, virtualInstances, childId, instance, unappliedPatch, deferredRefs)
|
||||
end
|
||||
|
||||
instance.Parent = parentInstance
|
||||
@@ -134,7 +143,6 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
end
|
||||
|
||||
local targetInstance = instanceMap.fromIds[refId]
|
||||
|
||||
if targetInstance == nil then
|
||||
markFailed(entry.id, entry.propertyName, entry.virtualValue)
|
||||
continue
|
||||
@@ -147,7 +155,4 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
reifyInstance = reifyInstance,
|
||||
applyDeferredRefs = applyDeferredRefs,
|
||||
}
|
||||
return reify
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
return function()
|
||||
local reify = require(script.Parent.reify)
|
||||
local reifyInstance, applyDeferredRefs = reify.reifyInstance, reify.applyDeferredRefs
|
||||
|
||||
local PatchSet = require(script.Parent.Parent.PatchSet)
|
||||
local InstanceMap = require(script.Parent.Parent.InstanceMap)
|
||||
@@ -21,11 +20,7 @@ return function()
|
||||
|
||||
it("should throw when given a bogus ID", function()
|
||||
expect(function()
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, {}, "Hi, mom!", game)
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
reify(InstanceMap.new(), {}, "Hi, mom!", game)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
@@ -39,11 +34,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT", nil)
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT", nil)
|
||||
|
||||
assert(instanceMap:size() == 0, "expected instanceMap to be empty")
|
||||
|
||||
@@ -68,11 +60,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -101,11 +90,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -136,11 +122,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
expect(size(unappliedPatch.added)).to.equal(1)
|
||||
expect(unappliedPatch.added["CHILD"]).to.equal(virtualInstances["CHILD"])
|
||||
@@ -170,11 +153,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
local instance = instanceMap.fromIds["ROOT"]
|
||||
expect(instance.ClassName).to.equal("StringValue")
|
||||
@@ -216,11 +196,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -246,16 +223,13 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
|
||||
local existing = Instance.new("Folder")
|
||||
existing.Name = "Existing"
|
||||
instanceMap:insert("EXISTING", existing)
|
||||
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -294,11 +268,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -336,11 +307,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(PatchSet.isEmpty(unappliedPatch), "expected remaining patch to be empty")
|
||||
|
||||
@@ -364,11 +332,8 @@ return function()
|
||||
},
|
||||
}
|
||||
|
||||
local deferredRefs = {}
|
||||
local instanceMap = InstanceMap.new()
|
||||
local unappliedPatch = reifyInstance(deferredRefs, instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
local unappliedPatch = reify(instanceMap, virtualInstances, "ROOT")
|
||||
|
||||
assert(not PatchSet.hasRemoves(unappliedPatch), "expected no removes")
|
||||
assert(not PatchSet.hasAdditions(unappliedPatch), "expected no additions")
|
||||
|
||||
@@ -7,7 +7,7 @@ local Log = require(Packages.Log)
|
||||
local RbxDom = require(Packages.RbxDom)
|
||||
local Error = require(script.Parent.Error)
|
||||
|
||||
local function setProperty(instance, propertyName, value)
|
||||
local function setProperty(instance: Instance, propertyName: string, value: unknown): boolean
|
||||
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||
|
||||
-- We can skip unknown properties; they're not likely reflected to Lua.
|
||||
@@ -28,6 +28,13 @@ local function setProperty(instance, propertyName, value)
|
||||
})
|
||||
end
|
||||
|
||||
if value == nil then
|
||||
if descriptor.dataType == "Float32" or descriptor.dataType == "Float64" then
|
||||
Log.trace("Skipping nil {} property {}.{}", descriptor.dataType, instance.ClassName, propertyName)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local writeSuccess, err = descriptor:write(instance, value)
|
||||
|
||||
if not writeSuccess then
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
||||
local SerializationService = game:GetService("SerializationService")
|
||||
local Selection = game:GetService("Selection")
|
||||
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local Log = require(Packages.Log)
|
||||
local Fmt = require(Packages.Fmt)
|
||||
local t = require(Packages.t)
|
||||
local Promise = require(Packages.Promise)
|
||||
local Timer = require(script.Parent.Timer)
|
||||
|
||||
local ChangeBatcher = require(script.Parent.ChangeBatcher)
|
||||
local encodePatchUpdate = require(script.Parent.ChangeBatcher.encodePatchUpdate)
|
||||
@@ -17,7 +13,6 @@ local InstanceMap = require(script.Parent.InstanceMap)
|
||||
local PatchSet = require(script.Parent.PatchSet)
|
||||
local Reconciler = require(script.Parent.Reconciler)
|
||||
local strict = require(script.Parent.strict)
|
||||
local Settings = require(script.Parent.Settings)
|
||||
|
||||
local Status = strict("Session.Status", {
|
||||
NotStarted = "NotStarted",
|
||||
@@ -55,6 +50,7 @@ ServeSession.Status = Status
|
||||
|
||||
local validateServeOptions = t.strictInterface({
|
||||
apiContext = t.table,
|
||||
openScriptsExternally = t.boolean,
|
||||
twoWaySync = t.boolean,
|
||||
})
|
||||
|
||||
@@ -93,14 +89,13 @@ function ServeSession.new(options)
|
||||
self = {
|
||||
__status = Status.NotStarted,
|
||||
__apiContext = options.apiContext,
|
||||
__openScriptsExternally = options.openScriptsExternally,
|
||||
__twoWaySync = options.twoWaySync,
|
||||
__reconciler = reconciler,
|
||||
__instanceMap = instanceMap,
|
||||
__changeBatcher = changeBatcher,
|
||||
__statusChangedCallback = nil,
|
||||
__connections = connections,
|
||||
__precommitCallbacks = {},
|
||||
__postcommitCallbacks = {},
|
||||
}
|
||||
|
||||
setmetatable(self, ServeSession)
|
||||
@@ -131,46 +126,12 @@ function ServeSession:setConfirmCallback(callback)
|
||||
self.__userConfirmCallback = callback
|
||||
end
|
||||
|
||||
--[=[
|
||||
Hooks a function to run before patch application.
|
||||
The provided function is called with the incoming patch and an InstanceMap
|
||||
as parameters.
|
||||
]=]
|
||||
function ServeSession:hookPrecommit(callback)
|
||||
table.insert(self.__precommitCallbacks, callback)
|
||||
Log.trace("Added precommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__precommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__precommitCallbacks, i)
|
||||
Log.trace("Removed precommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return self.__reconciler:hookPrecommit(callback)
|
||||
end
|
||||
|
||||
--[=[
|
||||
Hooks a function to run after patch application.
|
||||
The provided function is called with the applied patch, the current
|
||||
InstanceMap, and a PatchSet containing any unapplied changes.
|
||||
]=]
|
||||
function ServeSession:hookPostcommit(callback)
|
||||
table.insert(self.__postcommitCallbacks, callback)
|
||||
Log.trace("Added postcommit callback: {}", callback)
|
||||
|
||||
return function()
|
||||
-- Remove the callback from the list
|
||||
for i, cb in self.__postcommitCallbacks do
|
||||
if cb == callback then
|
||||
table.remove(self.__postcommitCallbacks, i)
|
||||
Log.trace("Removed postcommit callback: {}", callback)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return self.__reconciler:hookPostcommit(callback)
|
||||
end
|
||||
|
||||
function ServeSession:start()
|
||||
@@ -179,9 +140,10 @@ function ServeSession:start()
|
||||
self.__apiContext
|
||||
:connect()
|
||||
:andThen(function(serverInfo)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
return self:__initialSync(serverInfo):andThen(function()
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
return self:__mainSyncLoop()
|
||||
end)
|
||||
@@ -208,7 +170,7 @@ function ServeSession:__applyGameAndPlaceId(serverInfo)
|
||||
end
|
||||
|
||||
function ServeSession:__onActiveScriptChanged(activeScript)
|
||||
if not Settings:get("openScriptsExternally") then
|
||||
if not self.__openScriptsExternally then
|
||||
Log.trace("Not opening script {} because feature not enabled.", activeScript)
|
||||
|
||||
return
|
||||
@@ -246,169 +208,6 @@ function ServeSession:__onActiveScriptChanged(activeScript)
|
||||
self.__apiContext:open(scriptId)
|
||||
end
|
||||
|
||||
function ServeSession:__replaceInstances(idList)
|
||||
if #idList == 0 then
|
||||
return true, PatchSet.newEmpty()
|
||||
end
|
||||
-- It would be annoying if selection went away, so we try to preserve it.
|
||||
local selection = Selection:Get()
|
||||
local selectionMap = {}
|
||||
for i, instance in selection do
|
||||
selectionMap[instance] = i
|
||||
end
|
||||
|
||||
-- TODO: Should we do this in multiple requests so we can more granularly mark failures?
|
||||
local modelSuccess, replacements = self.__apiContext
|
||||
:serialize(idList)
|
||||
:andThen(function(response)
|
||||
Log.debug("Deserializing results from serialize endpoint")
|
||||
local objects = SerializationService:DeserializeInstancesAsync(response.modelContents)
|
||||
if not objects[1] then
|
||||
return Promise.reject("Serialize endpoint did not deserialize into any Instances")
|
||||
end
|
||||
if #objects[1]:GetChildren() ~= #idList then
|
||||
return Promise.reject("Serialize endpoint did not return the correct number of Instances")
|
||||
end
|
||||
|
||||
local instanceMap = {}
|
||||
for _, item in objects[1]:GetChildren() do
|
||||
instanceMap[item.Name] = item.Value
|
||||
end
|
||||
return instanceMap
|
||||
end)
|
||||
:await()
|
||||
|
||||
local refSuccess, refPatch = self.__apiContext
|
||||
:refPatch(idList)
|
||||
:andThen(function(response)
|
||||
return response.patch
|
||||
end)
|
||||
:await()
|
||||
|
||||
if not (modelSuccess and refSuccess) then
|
||||
return false
|
||||
end
|
||||
|
||||
for id, replacement in replacements do
|
||||
local oldInstance = self.__instanceMap.fromIds[id]
|
||||
self.__instanceMap:insert(id, replacement)
|
||||
Log.trace("Swapping Instance {} out via api/models/ endpoint", id)
|
||||
local oldParent = oldInstance.Parent
|
||||
for _, child in oldInstance:GetChildren() do
|
||||
child.Parent = replacement
|
||||
end
|
||||
|
||||
replacement.Parent = oldParent
|
||||
-- ChangeHistoryService doesn't like it if an Instance has been
|
||||
-- Destroyed. So, we have to accept the potential memory hit and
|
||||
-- just set the parent to `nil`.
|
||||
oldInstance.Parent = nil
|
||||
|
||||
if selectionMap[oldInstance] then
|
||||
-- This is a bit funky, but it saves the order of Selection
|
||||
-- which might matter for some use cases.
|
||||
selection[selectionMap[oldInstance]] = replacement
|
||||
end
|
||||
end
|
||||
|
||||
local patchApplySuccess, unappliedPatch = pcall(self.__reconciler.applyPatch, self.__reconciler, refPatch)
|
||||
if patchApplySuccess then
|
||||
Selection:Set(selection)
|
||||
return true, unappliedPatch
|
||||
else
|
||||
error(unappliedPatch)
|
||||
end
|
||||
end
|
||||
|
||||
function ServeSession:__applyPatch(patch)
|
||||
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
|
||||
local historyRecording = ChangeHistoryService:TryBeginRecording("Rojo: Patch " .. patchTimestamp)
|
||||
if not historyRecording then
|
||||
-- There can only be one recording at a time
|
||||
Log.debug("Failed to begin history recording for " .. patchTimestamp .. ". Another recording is in progress.")
|
||||
end
|
||||
|
||||
Timer.start("precommitCallbacks")
|
||||
-- Precommit callbacks must be serial in order to obey the contract that
|
||||
-- they execute before commit
|
||||
for _, callback in self.__precommitCallbacks do
|
||||
local success, err = pcall(callback, patch, self.__instanceMap)
|
||||
if not success then
|
||||
Log.warn("Precommit hook errored: {}", err)
|
||||
end
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
local patchApplySuccess, unappliedPatch = pcall(self.__reconciler.applyPatch, self.__reconciler, patch)
|
||||
if not patchApplySuccess then
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
-- This might make a weird stack trace but the only way applyPatch can
|
||||
-- fail is if a bug occurs so it's probably fine.
|
||||
error(unappliedPatch)
|
||||
end
|
||||
|
||||
if PatchSet.isEmpty(unappliedPatch) then
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local addedIdList = PatchSet.addedIdList(unappliedPatch)
|
||||
local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
|
||||
|
||||
local actualUnappliedPatches = PatchSet.newEmpty()
|
||||
if Settings:get("enableSyncFallback") then
|
||||
Log.debug("ServeSession:__replaceInstances(unappliedPatch.added)")
|
||||
Timer.start("ServeSession:__replaceInstances(unappliedPatch.added)")
|
||||
local addSuccess, unappliedAddedRefs = self:__replaceInstances(addedIdList)
|
||||
Timer.stop()
|
||||
|
||||
Log.debug("ServeSession:__replaceInstances(unappliedPatch.updated)")
|
||||
Timer.start("ServeSession:__replaceInstances(unappliedPatch.updated)")
|
||||
local updateSuccess, unappliedUpdateRefs = self:__replaceInstances(updatedIdList)
|
||||
Timer.stop()
|
||||
|
||||
if addSuccess then
|
||||
table.clear(unappliedPatch.added)
|
||||
PatchSet.assign(actualUnappliedPatches, unappliedAddedRefs)
|
||||
end
|
||||
if updateSuccess then
|
||||
table.clear(unappliedPatch.updated)
|
||||
PatchSet.assign(actualUnappliedPatches, unappliedUpdateRefs)
|
||||
end
|
||||
else
|
||||
Log.debug("Skipping ServeSession:__replaceInstances because of setting")
|
||||
end
|
||||
PatchSet.assign(actualUnappliedPatches, unappliedPatch)
|
||||
|
||||
if not PatchSet.isEmpty(actualUnappliedPatches) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
)
|
||||
end
|
||||
|
||||
Timer.start("postcommitCallbacks")
|
||||
-- Postcommit callbacks can be called with spawn since regardless of firing order, they are
|
||||
-- guaranteed to be called after the commit
|
||||
for _, callback in self.__postcommitCallbacks do
|
||||
task.spawn(function()
|
||||
local success, err = pcall(callback, patch, self.__instanceMap, actualUnappliedPatches)
|
||||
if not success then
|
||||
Log.warn("Postcommit hook errored: {}", err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
Timer.stop()
|
||||
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
end
|
||||
|
||||
function ServeSession:__initialSync(serverInfo)
|
||||
return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
|
||||
-- Tell the API Context that we're up-to-date with the version of
|
||||
@@ -483,7 +282,15 @@ function ServeSession:__initialSync(serverInfo)
|
||||
|
||||
return self.__apiContext:write(inversePatch)
|
||||
elseif userDecision == "Accept" then
|
||||
self:__applyPatch(catchUpPatch)
|
||||
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
)
|
||||
end
|
||||
|
||||
return Promise.resolve()
|
||||
else
|
||||
return Promise.reject("Invalid user decision: " .. userDecision)
|
||||
@@ -506,7 +313,14 @@ function ServeSession:__mainSyncLoop()
|
||||
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||
|
||||
for _, message in messages do
|
||||
self:__applyPatch(message)
|
||||
local unappliedPatch = self.__reconciler:applyPatch(message)
|
||||
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
)
|
||||
end
|
||||
end
|
||||
end)
|
||||
:await()
|
||||
|
||||
@@ -12,20 +12,13 @@ local Roact = require(Packages.Roact)
|
||||
local defaultSettings = {
|
||||
openScriptsExternally = false,
|
||||
twoWaySync = false,
|
||||
autoReconnect = false,
|
||||
showNotifications = true,
|
||||
enableSyncFallback = true,
|
||||
syncReminderMode = "Notify" :: "None" | "Notify" | "Fullscreen",
|
||||
syncReminderPolling = true,
|
||||
checkForUpdates = true,
|
||||
checkForPrereleases = false,
|
||||
autoConnectPlaytestServer = false,
|
||||
confirmationBehavior = "Initial" :: "Never" | "Initial" | "Large Changes" | "Unlisted PlaceId",
|
||||
syncReminder = true,
|
||||
confirmationBehavior = "Initial",
|
||||
largeChangesConfirmationThreshold = 5,
|
||||
playSounds = true,
|
||||
typecheckingEnabled = false,
|
||||
logLevel = "Info",
|
||||
timingLogsEnabled = false,
|
||||
priorEndpoints = {},
|
||||
}
|
||||
|
||||
@@ -111,14 +104,4 @@ function Settings:getBinding(name)
|
||||
return bind
|
||||
end
|
||||
|
||||
function Settings:getBindings(...: string)
|
||||
local bindings = {}
|
||||
for i = 1, select("#", ...) do
|
||||
local source = select(i, ...)
|
||||
bindings[source] = self:getBinding(source)
|
||||
end
|
||||
|
||||
return Roact.joinBindings(bindings)
|
||||
end
|
||||
|
||||
return Settings
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
local Settings = require(script.Parent.Settings)
|
||||
|
||||
local clock = os.clock
|
||||
|
||||
local Timer = {
|
||||
_entries = {},
|
||||
}
|
||||
|
||||
function Timer._start(label)
|
||||
local start = clock()
|
||||
if not label then
|
||||
error("[Rojo-Timer] Timer.start: label is required", 2)
|
||||
return
|
||||
end
|
||||
|
||||
table.insert(Timer._entries, { label, start })
|
||||
end
|
||||
|
||||
function Timer._stop()
|
||||
local stop = clock()
|
||||
|
||||
local entry = table.remove(Timer._entries)
|
||||
if not entry then
|
||||
error("[Rojo-Timer] Timer.stop: no label to stop", 2)
|
||||
return
|
||||
end
|
||||
|
||||
local label = entry[1]
|
||||
if #Timer._entries > 0 then
|
||||
local priorLabels = {}
|
||||
for _, priorEntry in ipairs(Timer._entries) do
|
||||
table.insert(priorLabels, priorEntry[1])
|
||||
end
|
||||
label = table.concat(priorLabels, "/") .. "/" .. label
|
||||
end
|
||||
|
||||
local start = entry[2]
|
||||
local duration = stop - start
|
||||
print(string.format("[Rojo-Timer] %s took %.3f ms", label, duration * 1000))
|
||||
end
|
||||
|
||||
-- Replace functions with no-op if not in debug mode
|
||||
local function no_op() end
|
||||
local function setFunctions(enabled)
|
||||
if enabled then
|
||||
Timer.start = Timer._start
|
||||
Timer.stop = Timer._stop
|
||||
else
|
||||
Timer.start = no_op
|
||||
Timer.stop = no_op
|
||||
end
|
||||
end
|
||||
|
||||
Settings:onChanged("timingLogsEnabled", setFunctions)
|
||||
setFunctions(Settings:get("timingLogsEnabled"))
|
||||
|
||||
return Timer
|
||||