├── .gitignore ├── .gitmodules ├── .prettierignore ├── LICENSE ├── README.md ├── bindings └── ts │ ├── client.ts │ ├── index.ts │ └── test.ts ├── build.ts ├── core ├── build │ ├── Makefile │ ├── apple-clang.sh │ └── windows.bat ├── go.mod ├── go.sum ├── main.go ├── src │ ├── archive │ │ └── main.go │ ├── config │ │ └── main.go │ ├── connect │ │ ├── channel.go │ │ └── main.go │ ├── esbuild │ │ ├── main.go │ │ └── resolve.go │ ├── fetch │ │ └── main.go │ ├── fs │ │ ├── main.go │ │ ├── virtual.go │ │ └── watcher.go │ ├── git │ │ ├── billy-fs.go │ │ ├── billy-storage.go │ │ └── main.go │ ├── methods │ │ └── main.go │ ├── packages │ │ ├── main.go │ │ └── package.go │ ├── serialize │ │ └── main.go │ ├── setup │ │ ├── callback.go │ │ └── directories.go │ ├── staticFiles │ │ └── main.go │ └── utils │ │ └── main.go └── wasm │ └── main.go ├── editor ├── assets │ ├── dev-icon.png │ └── seti.woff ├── code-editor.ts ├── commands │ └── index.ts ├── components │ ├── element.ts │ ├── refresheable.ts │ ├── top-bar.scss │ ├── top-bar.ts │ ├── view-scrollable.scss │ └── view-scrollable.ts ├── constants.ts ├── deeplink.ts ├── demo.ts ├── index.html ├── index.scss ├── index.ts ├── init.ts ├── lib │ ├── config │ │ ├── config.ts │ │ └── index.ts │ ├── core_open.ts │ ├── esbuild │ │ ├── esbuild.ts │ │ ├── index.ts │ │ └── sass.ts │ ├── fs_sync │ │ ├── fs_sync.ts │ │ └── index.ts │ ├── git │ │ ├── git.ts │ │ └── index.ts │ └── packages │ │ ├── index.ts │ │ └── packages.ts ├── stack-navigation.ts ├── store │ ├── editor.ts │ ├── index.ts │ ├── preferences.ts │ └── projects.ts ├── style │ ├── colors.scss │ ├── list.scss │ ├── spacing.scss │ ├── winbox.scss │ └── windows.scss ├── types │ └── index.ts ├── typescript │ ├── extensions.ts │ ├── index.ts │ └── worker.ts └── views │ ├── add-project │ ├── clone-git.ts │ ├── create-empty.ts │ ├── import-zip.ts │ ├── index.scss │ └── index.ts │ ├── packages │ ├── index.scss │ └── index.ts │ ├── project-settings.scss │ ├── project-settings.ts │ ├── project │ ├── dev-icons.scss │ ├── file-event.ts │ ├── file-tree.ts │ ├── git │ │ ├── auth │ │ │ ├── github.ts │ │ │ └── index.ts │ │ ├── branches.ts │ │ ├── index.scss │ │ └── index.ts │ ├── index.scss │ ├── index.ts │ └── prettier │ │ └── plugin-liquid.js │ ├── projects │ ├── index.scss │ ├── index.ts │ ├── list.ts │ ├── peers-widget.ts │ └── search-add.ts │ ├── prompt │ ├── index.scss │ └── index.ts │ └── settings │ ├── git-authentications.ts │ ├── index.scss │ ├── index.ts │ └── version.ts ├── lib ├── archive │ ├── archive.ts │ └── index.ts ├── base64.ts ├── bridge │ ├── index.ts │ ├── platform │ │ ├── android.ts │ │ ├── apple.ts │ │ ├── linux.ts │ │ ├── node.ts │ │ ├── wasm.ts │ │ └── windows.ts │ └── serialization.ts ├── components │ ├── snackbar.scss │ └── snackbar.ts ├── connect │ └── index.ts ├── core_message │ ├── core_message.ts │ └── index.ts ├── fetch │ └── index.ts ├── fs │ ├── fs.ts │ └── index.ts ├── fullstacked.d.ts └── platform │ └── index.ts ├── package-lock.json ├── package.json ├── platform ├── android │ ├── .gitignore │ ├── publish.js │ ├── studio │ │ ├── .gitignore │ │ ├── app │ │ │ ├── .gitignore │ │ │ ├── build.gradle.kts │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ └── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── cpp │ │ │ │ ├── CMakeLists.txt │ │ │ │ ├── bridge.cpp │ │ │ │ └── bridge.h │ │ │ │ ├── java │ │ │ │ └── org │ │ │ │ │ └── fullstacked │ │ │ │ │ └── editor │ │ │ │ │ ├── Instance.kt │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── WebViewComponent.kt │ │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ ├── fullstacked_app_icon_background.xml │ │ │ │ └── fullstacked_app_icon_foreground.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ │ └── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ ├── gradle │ │ │ ├── libs.versions.toml │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle.kts │ └── upload.py ├── apple │ ├── .gitignore │ ├── FullStacked.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── FullStacked │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── 100-bleed.png │ │ │ │ ├── 1024-bleed.png │ │ │ │ ├── 1024.png │ │ │ │ ├── 114-bleed.png │ │ │ │ ├── 120-bleed 1.png │ │ │ │ ├── 120-bleed.png │ │ │ │ ├── 128.png │ │ │ │ ├── 144-bleed.png │ │ │ │ ├── 152-bleed.png │ │ │ │ ├── 16.png │ │ │ │ ├── 167-bleed.png │ │ │ │ ├── 180-bleed.png │ │ │ │ ├── 20-bleed.png │ │ │ │ ├── 256.png │ │ │ │ ├── 29-bleed 1.png │ │ │ │ ├── 29-bleed.png │ │ │ │ ├── 32.png │ │ │ │ ├── 40-bleed 1.png │ │ │ │ ├── 40-bleed 2.png │ │ │ │ ├── 40-bleed.png │ │ │ │ ├── 50-bleed.png │ │ │ │ ├── 512.png │ │ │ │ ├── 57-bleed.png │ │ │ │ ├── 58-bleed 1.png │ │ │ │ ├── 58-bleed.png │ │ │ │ ├── 60-bleed.png │ │ │ │ ├── 64.png │ │ │ │ ├── 72-bleed.png │ │ │ │ ├── 76-bleed.png │ │ │ │ ├── 80-bleed 1.png │ │ │ │ ├── 80-bleed.png │ │ │ │ ├── 87-bleed.png │ │ │ │ └── Contents.json │ │ ├── FullStacked.entitlements │ │ ├── FullStacked.swift │ │ ├── Info.plist │ │ ├── Instance.swift │ │ ├── Main.swift │ │ ├── PrivacyInfo.xcprivacy │ │ ├── WebView.swift │ │ ├── WebViewAppKit.swift │ │ └── WebViewUIKit.swift │ └── publish.js ├── linux │ ├── .gitignore │ ├── app.cpp │ ├── app.h │ ├── build.sh │ ├── control │ ├── fix.sh │ ├── instance.cpp │ ├── instance.h │ ├── main.cpp │ ├── pkg.sh │ ├── utils.cpp │ └── utils.h ├── node │ ├── .gitignore │ ├── .npmignore │ ├── build.ts │ ├── package.json │ └── src │ │ ├── build.ts │ │ ├── call.ts │ │ ├── index.ts │ │ ├── instance.ts │ │ └── webview.ts ├── wasm │ ├── .gitignore │ ├── build.js │ ├── dev.js │ ├── package.json │ ├── publish.js │ └── src │ │ ├── dev-icon.png │ │ ├── index.html │ │ └── index.ts └── windows │ ├── .gitignore │ ├── App.xaml │ ├── App.xaml.cs │ ├── Assets │ ├── BadgeLogo.scale-100.png │ ├── BadgeLogo.scale-125.png │ ├── BadgeLogo.scale-150.png │ ├── BadgeLogo.scale-200.png │ ├── BadgeLogo.scale-400.png │ ├── Icon-16.ico │ ├── LargeTile.scale-100.png │ ├── LargeTile.scale-125.png │ ├── LargeTile.scale-150.png │ ├── LargeTile.scale-200.png │ ├── LargeTile.scale-400.png │ ├── SmallTile.scale-100.png │ ├── SmallTile.scale-125.png │ ├── SmallTile.scale-150.png │ ├── SmallTile.scale-200.png │ ├── SmallTile.scale-400.png │ ├── SplashScreen.scale-100.png │ ├── SplashScreen.scale-125.png │ ├── SplashScreen.scale-150.png │ ├── SplashScreen.scale-200.png │ ├── SplashScreen.scale-400.png │ ├── Square150x150Logo.scale-100.png │ ├── Square150x150Logo.scale-125.png │ ├── Square150x150Logo.scale-150.png │ ├── Square150x150Logo.scale-200.png │ ├── Square150x150Logo.scale-400.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png │ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png │ ├── Square44x44Logo.altform-unplated_targetsize-16.png │ ├── Square44x44Logo.altform-unplated_targetsize-24.png │ ├── Square44x44Logo.altform-unplated_targetsize-256.png │ ├── Square44x44Logo.altform-unplated_targetsize-32.png │ ├── Square44x44Logo.altform-unplated_targetsize-48.png │ ├── Square44x44Logo.scale-100.png │ ├── Square44x44Logo.scale-125.png │ ├── Square44x44Logo.scale-150.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.scale-400.png │ ├── Square44x44Logo.targetsize-16.png │ ├── Square44x44Logo.targetsize-16_altform-lightunplated.png │ ├── Square44x44Logo.targetsize-16_altform-unplated.png │ ├── Square44x44Logo.targetsize-24.png │ ├── Square44x44Logo.targetsize-24_altform-lightunplated.png │ ├── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── Square44x44Logo.targetsize-256.png │ ├── Square44x44Logo.targetsize-256_altform-lightunplated.png │ ├── Square44x44Logo.targetsize-256_altform-unplated.png │ ├── Square44x44Logo.targetsize-32.png │ ├── Square44x44Logo.targetsize-32_altform-lightunplated.png │ ├── Square44x44Logo.targetsize-32_altform-unplated.png │ ├── Square44x44Logo.targetsize-48.png │ ├── Square44x44Logo.targetsize-48_altform-lightunplated.png │ ├── Square44x44Logo.targetsize-48_altform-unplated.png │ ├── StoreLogo.scale-100.png │ ├── StoreLogo.scale-125.png │ ├── StoreLogo.scale-150.png │ ├── StoreLogo.scale-200.png │ ├── StoreLogo.scale-400.png │ ├── Wide310x150Logo.scale-100.png │ ├── Wide310x150Logo.scale-125.png │ ├── Wide310x150Logo.scale-150.png │ ├── Wide310x150Logo.scale-200.png │ └── Wide310x150Logo.scale-400.png │ ├── FullStacked.csproj │ ├── FullStacked.sln │ ├── Instance.cs │ ├── Lib.cs │ ├── LibARM64.cs │ ├── LibX64.cs │ ├── LibX86.cs │ ├── Package.appxmanifest │ ├── Properties │ ├── PublishProfiles │ │ ├── win-arm64.pubxml │ │ ├── win-x64.pubxml │ │ └── win-x86.pubxml │ └── launchSettings.json │ ├── WebView.cs │ ├── app.manifest │ └── publish.js ├── test ├── basic.ts ├── core.ts ├── deeplink-git.ts ├── index.ts ├── types.ts └── utils.ts ├── tsconfig.json └── version.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .DS_Store 4 | *.env 5 | bin 6 | out 7 | .vscode -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "demo"] 2 | path = demo 3 | url = https://github.com/fullstackedorg/demo.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | out 5 | demo 6 | 7 | editor/views/project/prettier/plugin-liquid.js 8 | 9 | **/*.cache 10 | platform/**/editor 11 | 12 | platform/android/studio/.gradle 13 | platform/android/studio/.idea 14 | platform/android/studio/app/.cxx 15 | platform/android/studio/app/build 16 | platform/android/studio/app/release 17 | 18 | platform/docker/package 19 | 20 | platform/electron/js 21 | 22 | platform/apple/*.xcarchive 23 | platform/apple/pkg* 24 | platform/apple/FullStacked.xcodeproj/xcshareddate 25 | platform/apple/FullStacked.xcodeproj/xcuserdata 26 | 27 | platform/windows -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # FullStacked 4 | 5 | **Code, Run, Share. Anywhere.** 6 | 7 | Create, run and share projects built with web technologies in a fully cross-platform, local-first environment. 8 | 9 | [Documentation](https://docs.fullstacked.org) | [Demo](https://demo.fullstacked.org) | [Roadmap](https://fullstacked.notion.site/FullStacked-Editor-Roadmap-ebfcb685b77446c7a7898c05b219215e) | [Figma](https://www.figma.com/design/xb3JBRCvEWpbwGda03T5QQ/Mockups) 10 | 11 | ![FullStacked](https://img.fullstacked.org/fullstacked.png) 12 | 13 | ## Installation 14 | 15 | ### Latest stable release is available on all major app marketplaces 16 | 17 | - [Apple App Store](https://apps.apple.com/ca/app/fullstacked/id6477835950) (MacOS, iOS, iPadOS) 18 | - [Google Play](https://play.google.com/store/apps/details?id=org.fullstacked.editor) (Android, Chromebook) 19 | - [Microsoft Store](https://apps.microsoft.com/detail/9p987qm508vc?hl=en-us) (Windows 10/11) 20 | 21 | You can always find those links and access to the beta apps on FullStacked [download page](https://fullstacked.org/download) 22 | 23 | ### Build from source 24 | 25 | #### Requirements 26 | 27 | - Go `1.23.1` 28 | - NodeJS `>=20` 29 | 30 | 1. Install npm dependencies `npm install` 31 | 2. Build the core. Move to the `core/build` directory and use `make` on UNIX-like (MacOS/Linux) or `./windows.bat` on Windows. 32 | 3. Run `npm start` at the root of the repository. 33 | 34 | ## License 35 | 36 | [GPL-3.0](https://github.com/fullstackedorg/fullstacked/blob/main/LICENSE) 37 | -------------------------------------------------------------------------------- /bindings/ts/client.ts: -------------------------------------------------------------------------------- 1 | import net from "net"; 2 | import { bytesToNumber, deserializeArgs, serializeArgs } from "../../lib/bridge/serialization"; 3 | import { Data, DataListener } from "."; 4 | 5 | type DataSocketClient = { 6 | socket: net.Socket, 7 | buffer: Uint8Array, 8 | listeners: Set 9 | } 10 | 11 | function trySend(dataSocketClient: DataSocketClient) { 12 | if (dataSocketClient.buffer.byteLength < 5) return false; 13 | const dataLength = bytesToNumber(dataSocketClient.buffer.slice(1, 5)); 14 | if (dataLength > dataSocketClient.buffer.byteLength - 5) return false; 15 | const data = deserializeArgs(dataSocketClient.buffer.slice(0, 5 + dataLength)); 16 | dataSocketClient.listeners.forEach(cb => cb(data.at(0))); 17 | dataSocketClient.buffer = dataSocketClient.buffer.slice(5 + dataLength); 18 | return true; 19 | } 20 | 21 | function onData(this: DataSocketClient, chunk: Buffer) { 22 | const bufferSize = this.buffer.byteLength + chunk.byteLength; 23 | const buffer = new Uint8Array(bufferSize); 24 | buffer.set(this.buffer); 25 | buffer.set(chunk, this.buffer.byteLength); 26 | this.buffer = buffer; 27 | 28 | let keepProcessing: boolean; 29 | do { 30 | keepProcessing = trySend(this); 31 | } while (keepProcessing && this.buffer.byteLength > 0) 32 | } 33 | 34 | export function connect( 35 | channel: string, 36 | port: number, 37 | host?: string 38 | ) { 39 | const dataSocketClient: DataSocketClient = { 40 | socket: new net.Socket(), 41 | buffer: new Uint8Array(), 42 | listeners: new Set() 43 | } 44 | dataSocketClient.socket.connect(port, host); 45 | dataSocketClient.socket.write(serializeArgs([channel])); 46 | dataSocketClient.socket.on("data", onData.bind(dataSocketClient)); 47 | 48 | const methods = { 49 | send(data: Data) { 50 | const serialized = serializeArgs(Array.isArray(data) ? data : [data]); 51 | dataSocketClient.socket.write(serialized); 52 | return methods; 53 | }, 54 | on(callback: DataListener) { 55 | dataSocketClient.listeners.add(callback); 56 | return methods; 57 | }, 58 | off(callback: DataListener) { 59 | dataSocketClient.listeners.delete(callback) 60 | return methods; 61 | } 62 | } 63 | 64 | return methods; 65 | } -------------------------------------------------------------------------------- /bindings/ts/test.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "."; 2 | 3 | const server = createServer(8888, "0.0.0.0"); 4 | console.log("Listening on 8888"); 5 | 6 | const channel = server.createChannel("test"); 7 | channel.on((data) => { 8 | if(data.at(0) === "ping") { 9 | console.log("pong"); 10 | setTimeout(() => channel.send("ping"), 1000); 11 | } 12 | }); 13 | 14 | // client 15 | // const channelClient = connect("test", 8888); 16 | // channelClient 17 | // .on(data => { 18 | // console.log(data); 19 | // if(data === "ping") { 20 | // console.log("Client pong"); 21 | // setTimeout(() => channelClient.send("ping"), 1000); 22 | // } 23 | // }) 24 | // .send("ping"); -------------------------------------------------------------------------------- /core/build/apple-clang.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # go/clangwrap.sh 4 | 5 | SDK_PATH=`xcrun --sdk $SDK --show-sdk-path` 6 | CLANG=`xcrun --sdk $SDK --find clang` 7 | 8 | if [ "$GOARCH" == "amd64" ]; then 9 | CARCH="x86_64" 10 | elif [ "$GOARCH" == "arm64" ]; then 11 | CARCH="arm64" 12 | fi 13 | 14 | exec $CLANG -arch $CARCH -isysroot $SDK_PATH "$@" -------------------------------------------------------------------------------- /core/build/windows.bat: -------------------------------------------------------------------------------- 1 | @ECHO off 2 | SET CGO_ENABLED ="1" 3 | SET GOOS ="windows" 4 | 5 | SET GOARCH ="arm64" 6 | go build -buildmode=c-shared -o ../bin/win-arm64.dll -v .. 7 | 8 | SET GOARCH ="amd64" 9 | go build -buildmode=c-shared -o ../bin/win-x64.dll -v .. 10 | 11 | SET GOARCH ="386" 12 | go build -buildmode=c-shared -o ../bin/win-x86.dll -v .. 13 | 14 | 15 | xcopy ..\bin\win-x86.dll ..\..\platform\windows /y /q 16 | xcopy ..\bin\win-x64.dll ..\..\platform\windows /y /q 17 | xcopy ..\bin\win-arm64.dll ..\..\platform\windows /y /q 18 | xcopy ..\..\out\editor ..\..\platform\windows\editor /y /s /e /q -------------------------------------------------------------------------------- /core/go.mod: -------------------------------------------------------------------------------- 1 | module fullstacked/editor 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/Masterminds/semver/v3 v3.3.1 // direct 7 | github.com/djherbis/times v1.6.0 // direct 8 | github.com/evanw/esbuild v0.25.3 // direct 9 | github.com/go-git/go-git/v5 v5.16.0 // direct 10 | ) 11 | 12 | require ( 13 | github.com/go-git/go-billy/v5 v5.6.2 14 | golang.org/x/net v0.39.0 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.1 // indirect 19 | github.com/Microsoft/go-winio v0.6.2 // indirect 20 | github.com/ProtonMail/go-crypto v1.2.0 // indirect 21 | github.com/cloudflare/circl v1.6.1 // indirect 22 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 23 | github.com/emirpasic/gods v1.18.1 // indirect 24 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 25 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 28 | github.com/kevinburke/ssh_config v1.2.0 // indirect 29 | github.com/pjbgf/sha1cd v0.3.2 // indirect 30 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 31 | github.com/skeema/knownhosts v1.3.1 // indirect 32 | github.com/xanzy/ssh-agent v0.3.3 // indirect 33 | golang.org/x/crypto v0.37.0 // indirect 34 | golang.org/x/mod v0.24.0 // indirect 35 | golang.org/x/sync v0.13.0 // indirect 36 | golang.org/x/sys v0.32.0 // indirect 37 | golang.org/x/tools v0.32.0 // indirect 38 | google.golang.org/protobuf v1.36.6 // indirect 39 | gopkg.in/warnings.v0 v0.1.2 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /core/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include 5 | 6 | typedef void (*Callback)(char *projectId, char* type, char *msg); 7 | static inline void CallMyFunction(void *callback, char *projectId, char * type, char *msg) { 8 | ((Callback)callback)(projectId, type, msg); 9 | } 10 | */ 11 | import "C" 12 | 13 | import ( 14 | fs "fullstacked/editor/src/fs" 15 | methods "fullstacked/editor/src/methods" 16 | setup "fullstacked/editor/src/setup" 17 | "unsafe" 18 | ) 19 | 20 | func main() {} 21 | 22 | //export directories 23 | func directories(root *C.char, 24 | config *C.char, 25 | editor *C.char) { 26 | setup.SetupDirectories( 27 | C.GoString(root), 28 | C.GoString(config), 29 | C.GoString(editor), 30 | ) 31 | 32 | fileEventOrigin := "setup" 33 | fs.Mkdir(setup.Directories.Root, fileEventOrigin) 34 | fs.Mkdir(setup.Directories.Config, fileEventOrigin) 35 | fs.Mkdir(setup.Directories.Editor, fileEventOrigin) 36 | 37 | // clean tmp 38 | fs.Rmdir(setup.Directories.Tmp, fileEventOrigin) 39 | fs.Mkdir(setup.Directories.Tmp, fileEventOrigin) 40 | } 41 | 42 | var cCallback = (unsafe.Pointer)(nil) 43 | 44 | //export callback 45 | func callback(cb unsafe.Pointer) { 46 | cCallback = cb 47 | 48 | setup.Callback = func(projectId string, messageType string, message string) { 49 | projectIdPtr := C.CString(projectId) 50 | messageTypePtr := C.CString(messageType) 51 | messagePtr := C.CString(message) 52 | 53 | C.CallMyFunction( 54 | cCallback, 55 | projectIdPtr, 56 | messageTypePtr, 57 | messagePtr, 58 | ) 59 | 60 | C.free(unsafe.Pointer(projectIdPtr)) 61 | C.free(unsafe.Pointer(messageTypePtr)) 62 | C.free(unsafe.Pointer(messagePtr)) 63 | } 64 | } 65 | 66 | //export call 67 | func call(buffer unsafe.Pointer, length C.int, responsePtr *unsafe.Pointer) C.int { 68 | response := methods.Call(C.GoBytes(buffer, length)) 69 | *responsePtr = C.CBytes(response) 70 | return C.int(len(response)) 71 | } 72 | 73 | //export freePtr 74 | func freePtr(ptr unsafe.Pointer) { 75 | C.free(ptr) 76 | } 77 | -------------------------------------------------------------------------------- /core/src/config/main.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | fs "fullstacked/editor/src/fs" 5 | serialize "fullstacked/editor/src/serialize" 6 | setup "fullstacked/editor/src/setup" 7 | "path" 8 | ) 9 | 10 | var fileEventOrigin = "config" 11 | 12 | func Get(configFile string) ([]byte, error) { 13 | filePath := path.Join(setup.Directories.Config, configFile+".json") 14 | 15 | config, err := fs.ReadFile(filePath) 16 | 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return config, nil 22 | } 23 | 24 | func GetSerialized(configFile string) []byte { 25 | config, err := Get(configFile) 26 | 27 | if err != nil { 28 | return nil 29 | } 30 | 31 | return serialize.SerializeString(string(config)) 32 | } 33 | 34 | func SaveSerialized(configFile string, data string) []byte { 35 | filePath := path.Join(setup.Directories.Config, configFile+".json") 36 | 37 | fs.Mkdir(path.Dir(filePath), fileEventOrigin) 38 | 39 | err := fs.WriteFile(filePath, []byte(data), fileEventOrigin) 40 | 41 | if err != nil { 42 | return serialize.SerializeBoolean(false) 43 | } 44 | 45 | return serialize.SerializeBoolean(true) 46 | } 47 | -------------------------------------------------------------------------------- /core/src/connect/channel.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "fmt" 7 | "fullstacked/editor/src/serialize" 8 | "fullstacked/editor/src/setup" 9 | "net" 10 | "strconv" 11 | ) 12 | 13 | type Channel struct { 14 | ProjectId string 15 | Id string 16 | Name string 17 | Port int 18 | Host string 19 | Raw bool 20 | buffer []byte 21 | conn net.Conn 22 | } 23 | 24 | func (c *Channel) connect() { 25 | fmt.Println("Connecting to " + c.Host + ":" + strconv.Itoa(c.Port)) 26 | 27 | conn, err := net.Dial("tcp", c.Host+":"+strconv.Itoa(c.Port)) 28 | if err != nil { 29 | fmt.Println(err) 30 | return 31 | } 32 | 33 | _, err = conn.Write(serialize.SerializeString(c.Name)) 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | 39 | c.conn = conn 40 | } 41 | 42 | func (c *Channel) start() { 43 | if c.conn == nil { 44 | return 45 | } 46 | 47 | for { 48 | buf := make([]byte, 1024) 49 | size, err := bufio.NewReader(c.conn).Read(buf) 50 | if err != nil { 51 | fmt.Println(err.Error()) 52 | return 53 | } 54 | 55 | if c.Raw { 56 | setup.Callback(c.ProjectId, "channel-"+c.Id, base64.RawStdEncoding.EncodeToString(buf[0:size])) 57 | } else { 58 | c.buffer = append(c.buffer, buf[0:size]...) 59 | c.receive() 60 | } 61 | } 62 | } 63 | 64 | func (c *Channel) receive() { 65 | if len(c.buffer) < 4 { 66 | return 67 | } 68 | 69 | bodyLength := serialize.DeserializeBytesToInt(c.buffer[0:4]) 70 | if len(c.buffer) < bodyLength+4 { 71 | return 72 | } 73 | 74 | body := c.buffer[4 : 4+bodyLength] 75 | setup.Callback(c.ProjectId, "channel-"+c.Id, base64.RawStdEncoding.EncodeToString(body)) 76 | 77 | c.buffer = c.buffer[4+bodyLength:] 78 | if len(c.buffer) > 0 { 79 | c.receive() 80 | } 81 | } 82 | 83 | func (c *Channel) send(data []byte) { 84 | if c.conn == nil { 85 | return 86 | } 87 | c.conn.Write(data) 88 | } 89 | -------------------------------------------------------------------------------- /core/src/connect/main.go: -------------------------------------------------------------------------------- 1 | package connect 2 | 3 | import ( 4 | "fullstacked/editor/src/utils" 5 | ) 6 | 7 | var channels = map[string]Channel{} 8 | 9 | func Connect( 10 | projectId string, 11 | name string, 12 | port float64, 13 | host string, 14 | raw bool, 15 | ) string { 16 | channelId := utils.RandString(6) 17 | channel := Channel{ 18 | ProjectId: projectId, 19 | Id: channelId, 20 | Name: name, 21 | Port: int(port), 22 | Host: host, 23 | Raw: raw, 24 | } 25 | channel.connect() 26 | channels[channelId] = channel 27 | go channel.start() 28 | return channelId 29 | } 30 | 31 | func Send( 32 | channelId string, 33 | data []byte, 34 | ) { 35 | channel, ok := channels[channelId] 36 | if !ok { 37 | return 38 | } 39 | channel.send(data) 40 | } 41 | -------------------------------------------------------------------------------- /core/src/fs/watcher.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "encoding/json" 5 | "fullstacked/editor/src/setup" 6 | "fullstacked/editor/src/utils" 7 | "path/filepath" 8 | "time" 9 | ) 10 | 11 | type FileEventType int 12 | 13 | const ( 14 | UNKNOWN FileEventType = 0 15 | CREATED FileEventType = 1 16 | MODIFIED FileEventType = 2 17 | RENAME FileEventType = 3 18 | DELETED FileEventType = 4 19 | ) 20 | 21 | type FileEvent struct { 22 | Type FileEventType `json:"type"` 23 | Paths []string `json:"paths"` 24 | IsFile bool `json:"isFile"` 25 | Origin string `json:"origin"` 26 | } 27 | 28 | var eventsBuf = []FileEvent{} 29 | var debounce = utils.NewDebouncer(time.Millisecond * 100) // 100ms 30 | 31 | var sendEvents = func() func() { 32 | return func() { 33 | jsonData, _ := json.Marshal(eventsBuf) 34 | setup.Callback("", "file-event", string(jsonData)) 35 | eventsBuf = []FileEvent{} 36 | } 37 | } 38 | 39 | func watchEvent(event FileEvent) { 40 | for i, p := range event.Paths { 41 | event.Paths[i] = filepath.ToSlash(p) 42 | } 43 | eventsBuf = append(eventsBuf, event) 44 | debounce(sendEvents()) 45 | } 46 | -------------------------------------------------------------------------------- /core/src/serialize/main.go: -------------------------------------------------------------------------------- 1 | package serialize 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | ) 7 | 8 | const ( 9 | UNDEFINED = 0 10 | BOOLEAN = 1 11 | STRING = 2 12 | NUMBER = 3 13 | BUFFER = 4 14 | ERROR = 5 15 | ) 16 | 17 | func DeserializeBytesToInt(bytes []byte) int { 18 | return int((uint(bytes[0]) << 24) | 19 | (uint(bytes[1]) << 16) | 20 | (uint(bytes[2]) << 8) | 21 | (uint(bytes[3]) << 0)) 22 | } 23 | 24 | func SerializeIntToBytes(num int) []byte { 25 | bytes := []byte{0, 0, 0, 0} 26 | bytes[0] = uint8((uint(num) & uint(0xff000000)) >> 24) 27 | bytes[1] = uint8((num & 0x00ff0000) >> 16) 28 | bytes[2] = uint8((num & 0x0000ff00) >> 8) 29 | bytes[3] = uint8((num & 0x000000ff) >> 0) 30 | return bytes 31 | } 32 | 33 | func SerializeNumber(num float64) []byte { 34 | bytes := []byte{NUMBER} 35 | bytes = append(bytes, SerializeIntToBytes(8)...) 36 | float64Bytes := make([]byte, 8) 37 | binary.BigEndian.PutUint64(float64Bytes[:], math.Float64bits(num)) 38 | bytes = append(bytes, float64Bytes...) 39 | return bytes 40 | } 41 | 42 | func SerializeBoolean(value bool) []byte { 43 | bytes := []byte{BOOLEAN} 44 | bytes = append(bytes, SerializeIntToBytes(1)...) 45 | if value { 46 | bytes = append(bytes, 1) 47 | } else { 48 | bytes = append(bytes, 0) 49 | } 50 | return bytes 51 | } 52 | 53 | func SerializeString(str string) []byte { 54 | bytes := []byte{STRING} 55 | strData := []byte(str) 56 | bytes = append(bytes, SerializeIntToBytes(len(strData))...) 57 | bytes = append(bytes, strData...) 58 | return bytes 59 | } 60 | 61 | func SerializeBuffer(buffer []byte) []byte { 62 | bytes := []byte{BUFFER} 63 | bytes = append(bytes, SerializeIntToBytes(len(buffer))...) 64 | bytes = append(bytes, buffer...) 65 | return bytes 66 | } 67 | 68 | func SerializeError(err error) []byte { 69 | bytes := []byte{ERROR} 70 | errStrData := []byte(err.Error()) 71 | bytes = append(bytes, SerializeIntToBytes(len(errStrData))...) 72 | bytes = append(bytes, errStrData...) 73 | return bytes 74 | } 75 | 76 | func DeserializeNumber(bytes []byte) float64 { 77 | bits := binary.BigEndian.Uint64(bytes) 78 | float := math.Float64frombits(bits) 79 | return float 80 | } 81 | 82 | func DeserializeArgs(data []byte) (int, []any) { 83 | cursor := 0 84 | 85 | method := int(data[cursor]) 86 | cursor++ 87 | 88 | var args []any 89 | 90 | for cursor < len(data) { 91 | argType := int(data[cursor]) 92 | cursor++ 93 | argLength := DeserializeBytesToInt(data[cursor : cursor+4]) 94 | cursor += 4 95 | argData := data[cursor : cursor+argLength] 96 | cursor += argLength 97 | 98 | switch argType { 99 | case UNDEFINED: 100 | args = append(args, nil) 101 | case BOOLEAN: 102 | args = append(args, argData[0] == 1) 103 | case STRING: 104 | args = append(args, string(argData)) 105 | case NUMBER: 106 | args = append(args, DeserializeNumber(argData)) 107 | case BUFFER: 108 | args = append(args, argData) 109 | } 110 | 111 | } 112 | 113 | return method, args 114 | } 115 | -------------------------------------------------------------------------------- /core/src/setup/callback.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | var Callback = (func(string, string, string))(nil) 4 | -------------------------------------------------------------------------------- /core/src/setup/directories.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "path" 5 | ) 6 | 7 | type DirectoriesStruct struct { 8 | Root string 9 | Config string 10 | Tmp string 11 | Editor string 12 | } 13 | 14 | var Directories *DirectoriesStruct = nil 15 | 16 | func SetupDirectories(root string, config string, editor string) { 17 | Directories = &DirectoriesStruct{ 18 | Root: root, 19 | Config: config, 20 | Tmp: path.Join(root, ".tmp"), 21 | Editor: editor, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/utils/main.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 10 | 11 | func RandString(n int) string { 12 | b := make([]rune, n) 13 | for i := range b { 14 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 15 | } 16 | return string(b) 17 | } 18 | 19 | // source: https://stackoverflow.com/a/77944299 20 | func NewDebouncer(dur time.Duration) func(fn func()) { 21 | d := &debouncer{ 22 | dur: dur, 23 | } 24 | 25 | return func(fn func()) { 26 | d.reset(fn) 27 | } 28 | } 29 | 30 | type debouncer struct { 31 | mu sync.Mutex 32 | dur time.Duration 33 | delay *time.Timer 34 | } 35 | 36 | func (d *debouncer) reset(fn func()) { 37 | d.mu.Lock() 38 | defer d.mu.Unlock() 39 | 40 | if d.delay != nil { 41 | d.delay.Stop() 42 | } 43 | 44 | d.delay = time.AfterFunc(d.dur, fn) 45 | } 46 | -------------------------------------------------------------------------------- /core/wasm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | fs "fullstacked/editor/src/fs" 7 | methods "fullstacked/editor/src/methods" 8 | setup "fullstacked/editor/src/setup" 9 | "strings" 10 | 11 | "syscall/js" 12 | ) 13 | 14 | func directories(this js.Value, args []js.Value) interface{} { 15 | setup.SetupDirectories( 16 | args[0].String(), 17 | args[1].String(), 18 | args[2].String(), 19 | ) 20 | return nil 21 | } 22 | 23 | func call(this js.Value, args []js.Value) interface{} { 24 | payload := make([]byte, args[0].Get("length").Int()) 25 | _ = js.CopyBytesToGo(payload, args[0]) 26 | 27 | handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { 28 | resolve := args[0] 29 | go func() { 30 | response := methods.Call(payload) 31 | resolve.Invoke(base64.StdEncoding.EncodeToString(response)) 32 | }() 33 | return nil 34 | }) 35 | 36 | promiseConstructor := js.Global().Get("Promise") 37 | return promiseConstructor.New(handler) 38 | } 39 | 40 | func vfs(this js.Value, args []js.Value) interface{} { 41 | fileMap := make(map[string]interface{}) 42 | 43 | prefix := "" 44 | if len(args) == 1 { 45 | prefix = args[0].String() 46 | } 47 | 48 | arrayConstructor := js.Global().Get("Uint8Array") 49 | 50 | for name, f := range fs.VirtFS { 51 | if !strings.HasPrefix(name, prefix) { 52 | continue 53 | } 54 | 55 | dataJS := arrayConstructor.New(len(f.Data)) 56 | js.CopyBytesToJS(dataJS, f.Data) 57 | 58 | fileMap[name] = dataJS 59 | } 60 | 61 | for _, dir := range fs.VirtDirs { 62 | if !strings.HasPrefix(dir, prefix) { 63 | continue 64 | } 65 | 66 | fileMap[dir] = nil 67 | } 68 | 69 | return js.ValueOf(fileMap) 70 | } 71 | 72 | func callback(projectId string, messageType string, message string) { 73 | js.Global().Call("onmessageWASM", js.ValueOf(projectId), js.ValueOf(messageType), js.ValueOf(message)) 74 | } 75 | 76 | func main() { 77 | c := make(chan struct{}, 0) 78 | 79 | fmt.Println("FullStacked WASM") 80 | fs.WASM = true 81 | 82 | setup.Callback = callback 83 | js.Global().Set("directories", js.FuncOf(directories)) 84 | js.Global().Set("call", js.FuncOf(call)) 85 | js.Global().Set("vfs", js.FuncOf(vfs)) 86 | 87 | <-c 88 | } 89 | -------------------------------------------------------------------------------- /editor/assets/dev-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/editor/assets/dev-icon.png -------------------------------------------------------------------------------- /editor/assets/seti.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/editor/assets/seti.woff -------------------------------------------------------------------------------- /editor/components/element.ts: -------------------------------------------------------------------------------- 1 | export type ElementComponent = T & { 2 | destroy: () => void; 3 | ondestroy: () => void; 4 | }; 5 | 6 | export function createElement( 7 | element: T 8 | ) { 9 | const e = document.createElement(element) as ElementComponent< 10 | HTMLElementTagNameMap[T] 11 | >; 12 | e.destroy = () => e.ondestroy?.(); 13 | return e; 14 | } 15 | -------------------------------------------------------------------------------- /editor/components/refresheable.ts: -------------------------------------------------------------------------------- 1 | import { createElement, ElementComponent } from "./element"; 2 | 3 | export function createRefresheable< 4 | T extends (...args: any) => ElementComponent | Promise, 5 | P extends Parameters 6 | >( 7 | elementRenderer: ( 8 | ...args: P 9 | ) => ElementComponent | Promise, 10 | placeholder?: ElementComponent 11 | ) { 12 | const refresheable = { 13 | element: (placeholder || 14 | (createElement("div") as ElementComponent)) as Awaited< 15 | ReturnType 16 | >, 17 | refresh: (...newArgs: P) => { 18 | refresheable.element.destroy(); 19 | const updatedElement = elementRenderer(...newArgs); 20 | if (updatedElement instanceof Promise) { 21 | return new Promise((resolve) => { 22 | updatedElement.then((e) => { 23 | refresheable.element.replaceWith(e); 24 | refresheable.element = e; 25 | resolve(); 26 | }); 27 | }); 28 | } else { 29 | refresheable.element.replaceWith(updatedElement); 30 | refresheable.element = updatedElement; 31 | } 32 | } 33 | }; 34 | 35 | return refresheable; 36 | } 37 | -------------------------------------------------------------------------------- /editor/components/top-bar.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | 3 | .top-bar { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | 8 | width: calc(100% + spacing.$medium); 9 | margin-left: 0 - spacing.$medium; 10 | &.no-back { 11 | width: 100%; 12 | margin-left: 0; 13 | } 14 | 15 | > div:first-child { 16 | display: flex; 17 | align-items: flex-start; 18 | justify-content: flex-start; 19 | 20 | overflow: hidden; 21 | flex: 1; 22 | 23 | .titles { 24 | display: flex; 25 | flex-direction: column; 26 | align-items: flex-start; 27 | justify-content: center; 28 | white-space: nowrap; 29 | overflow: hidden; 30 | 31 | > * { 32 | width: 100%; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | } 36 | } 37 | } 38 | 39 | > div:last-child { 40 | display: flex; 41 | align-items: center; 42 | justify-content: flex-end; 43 | gap: spacing.$small; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /editor/components/top-bar.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "@fullstacked/ui"; 2 | import { BACK_BUTTON_CLASS } from "../constants"; 3 | import stackNavigation from "../stack-navigation"; 4 | import { createElement } from "./element"; 5 | 6 | type TopBarOpts = { 7 | noBack: boolean; 8 | title: string; 9 | subtitle: string; 10 | actions: HTMLElement[]; 11 | onBack: () => boolean; 12 | }; 13 | 14 | let h1Height: number, getH1HeightInterval: ReturnType; 15 | 16 | export function TopBar(opts?: Partial) { 17 | const container = createElement("div"); 18 | container.classList.add("top-bar"); 19 | 20 | const left = document.createElement("div"); 21 | 22 | let backButton: HTMLButtonElement; 23 | if (!opts?.noBack) { 24 | backButton = Button({ 25 | style: "icon-large", 26 | iconLeft: "Arrow" 27 | }); 28 | backButton.classList.add(BACK_BUTTON_CLASS); 29 | backButton.onclick = () => { 30 | if (opts?.onBack && !opts.onBack()) { 31 | return; 32 | } 33 | 34 | stackNavigation.back(); 35 | }; 36 | left.append(backButton); 37 | } else { 38 | container.classList.add("no-back"); 39 | } 40 | 41 | const titlesContainer = document.createElement("div"); 42 | titlesContainer.classList.add("titles"); 43 | left.append(titlesContainer); 44 | 45 | let title: HTMLHeadingElement; 46 | if (opts?.title) { 47 | title = document.createElement("h1"); 48 | title.innerText = opts.title; 49 | titlesContainer.append(title); 50 | } 51 | if (opts?.subtitle) { 52 | const subtitle = document.createElement("p"); 53 | subtitle.innerText = opts.subtitle; 54 | titlesContainer.append(subtitle); 55 | } 56 | 57 | const right = document.createElement("div"); 58 | right.classList.add("top-bar-actions"); 59 | 60 | if (opts?.actions) { 61 | right.append(...opts.actions); 62 | } 63 | 64 | container.append(left, right); 65 | 66 | const setHeight = () => { 67 | if (!h1Height) { 68 | const getH1Height = () => { 69 | if (h1Height) { 70 | clearInterval(getH1HeightInterval); 71 | getH1HeightInterval = null; 72 | return; 73 | } 74 | 75 | h1Height = title.getBoundingClientRect().height; 76 | setHeight(); 77 | }; 78 | getH1HeightInterval = setInterval(getH1Height, 1000); 79 | } else { 80 | if (backButton) { 81 | backButton.style.height = h1Height + "px"; 82 | } 83 | 84 | titlesContainer.style.minHeight = h1Height + "px"; 85 | } 86 | }; 87 | setHeight(); 88 | 89 | return container; 90 | } 91 | -------------------------------------------------------------------------------- /editor/components/view-scrollable.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | 3 | .view-scrollable { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | overflow: hidden; 8 | 9 | .scrollable { 10 | overflow: auto; 11 | margin-left: 0 - spacing.$medium; 12 | margin-right: 0 - spacing.$medium; 13 | margin-bottom: 0 - spacing.$medium; 14 | padding-left: spacing.$medium; 15 | padding-right: spacing.$medium; 16 | padding-bottom: spacing.$medium; 17 | display: flex; 18 | flex-direction: column; 19 | flex: 1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /editor/components/view-scrollable.ts: -------------------------------------------------------------------------------- 1 | export function ViewScrollable() { 2 | const container = document.createElement("div"); 3 | container.classList.add("view-scrollable"); 4 | 5 | const scrollable = document.createElement("div"); 6 | scrollable.classList.add("scrollable"); 7 | 8 | container.append(scrollable); 9 | 10 | return { container, scrollable }; 11 | } 12 | -------------------------------------------------------------------------------- /editor/constants.ts: -------------------------------------------------------------------------------- 1 | export const PROJECTS_TITLE = "Projects"; 2 | export const PROJECTS_VIEW_ID = "projects-view"; 3 | export const PROJECT_VIEW_ID = "project"; 4 | export const PEERS_VIEW_ID = "peers"; 5 | export const SETTINGS_VIEW_ID = "settings"; 6 | export const NEW_PROJECT_ID = "new-project"; 7 | export const IMPORT_ZIP_ID = "import-zip"; 8 | export const IMPORT_PROJECT_FILE_INPUT_ID = "import-project"; 9 | export const RUN_PROJECT_ID = "run-project"; 10 | export const NEW_FILE_ID = "new-file"; 11 | export const SETTINGS_BUTTON_ID = "settings"; 12 | export const BACK_BUTTON_CLASS = "back-button"; 13 | export const PROJECT_TITLE_ID = "project-title"; 14 | export const TYPESCRIPT_ICON_ID = "typescript-icon"; 15 | export const PEERS_BUTTON_ID = "peers-connectivity"; 16 | export const INCOMING_PEER_CONNECTION_REQUEST_DIALOG = 17 | "peer-connection-request-dialog"; 18 | export const MANUAL_PEER_CONNECT_DIALOG = "manual-peer-connect"; 19 | export const PEER_PAIR_BUTTON_CLASS = "peer-pair"; 20 | export const PEER_PAIRING_CODE_CLASS = "peer-pairing-code"; 21 | export const PEER_TRUST_BUTTON_ID = "peer-trust"; 22 | export const PEER_DISCONNECT_BUTTON_CLASS = "peer-disconnect"; 23 | export const PEER_CONNECTIVITY_BACK_BUTTON_ID = "peer-back"; 24 | export const BG_COLOR = "#1e293b"; 25 | -------------------------------------------------------------------------------- /editor/deeplink.ts: -------------------------------------------------------------------------------- 1 | import { Button, Dialog } from "@fullstacked/ui"; 2 | import { createElement } from "./components/element"; 3 | import config from "./lib/config"; 4 | import { Store } from "./store"; 5 | import { CONFIG_TYPE, Project as ProjectType } from "./types"; 6 | import { CloneGit } from "./views/add-project/clone-git"; 7 | import { Project } from "./views/project"; 8 | 9 | // fullstacked://http//github.....git 10 | export async function deeplink(fullstackedUrl: string) { 11 | console.log(fullstackedUrl); 12 | 13 | let url = fullstackedUrl.slice("fullstacked://".length); 14 | 15 | const [protocol, ...rest] = url.split("//"); 16 | const [hostAndPath] = rest.join("//").split("?"); 17 | url = protocol + (protocol.endsWith(":") ? "" : ":") + "//" + hostAndPath; 18 | 19 | if (!url.endsWith(".git")) { 20 | url += ".git"; 21 | } 22 | 23 | const runProjectIfFound = (projects: ProjectType[]) => { 24 | console.log(projects, url); 25 | const existingProject = projects?.find( 26 | (p) => p.gitRepository?.url === url 27 | ); 28 | if (existingProject) { 29 | Store.projects.list.unsubscribe(runProjectIfFound); 30 | 31 | let isUserMode = Store.preferences.isUserMode.check(); 32 | if (!isUserMode) { 33 | Project(existingProject); 34 | } 35 | 36 | Store.projects.build(existingProject); 37 | return true; 38 | } 39 | 40 | return false; 41 | }; 42 | 43 | const { projects } = await config.get(CONFIG_TYPE.PROJECTS); 44 | 45 | if (runProjectIfFound(projects)) return; 46 | 47 | Store.projects.list.subscribe(runProjectIfFound); 48 | CloneGit(url); 49 | } 50 | 51 | export function WindowsAskForAdmin() { 52 | const container = createElement("div"); 53 | container.classList.add("win-admin-dialog"); 54 | container.innerHTML = ` 55 |

Welcome,

56 |

Please close FullStacked and reopen it with Run as administrator.

57 |

It will register the FullStacked deeplink and enable the Open in FullStacked feature in your system.

58 |

You can open FullStacked normally afterwards.

59 |

You only have to do this operation once.

60 | `; 61 | const closeButton = Button({ 62 | text: "Close" 63 | }); 64 | container.append(closeButton); 65 | const { remove } = Dialog(container); 66 | closeButton.onclick = () => { 67 | remove(); 68 | fetch("/restart-admin"); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /editor/demo.ts: -------------------------------------------------------------------------------- 1 | import { bridge } from "../lib/bridge"; 2 | import { serializeArgs } from "../lib/bridge/serialization"; 3 | import core_fetch from "../lib/fetch"; 4 | import core_message from "../lib/core_message"; 5 | import git from "./lib/git"; 6 | import { 7 | createAndMoveProject, 8 | randomStr, 9 | tmpDir 10 | } from "./views/add-project/import-zip"; 11 | import archive from "../lib/archive"; 12 | 13 | export async function Demo() { 14 | try { 15 | await core_fetch("https://github.com/fullstackedorg", { timeout: 3 }); 16 | } catch (e) { 17 | return demoFromZip(); 18 | } 19 | 20 | return demoFromGitHub(); 21 | } 22 | 23 | async function demoFromZip() { 24 | const payload = new Uint8Array([ 25 | 1, // static file serving 26 | 27 | ...serializeArgs(["Demo.zip"]) 28 | ]); 29 | 30 | const [_, demoZipData] = (await bridge(payload)) as [string, Uint8Array]; 31 | const tmpDirectory = tmpDir + "/" + randomStr(6); 32 | await archive.unzip(demoZipData, tmpDirectory); 33 | createAndMoveProject( 34 | tmpDirectory, 35 | { 36 | container: null, 37 | logger: () => {}, 38 | text: null 39 | }, 40 | "Demo", 41 | null 42 | ); 43 | } 44 | 45 | const demoRepoUrl = "https://github.com/fullstackedorg/demo.git"; 46 | 47 | async function demoFromGitHub() { 48 | let checkForDone: (message: string) => void; 49 | const donePromise = new Promise((resolve) => { 50 | checkForDone = (gitProgress: string) => { 51 | let json: { Url: string; Data: string }; 52 | try { 53 | json = JSON.parse(gitProgress); 54 | } catch (e) { 55 | return; 56 | } 57 | 58 | if (json.Url !== demoRepoUrl) return; 59 | 60 | if (json.Data.trim().endsWith("done")) { 61 | resolve(); 62 | } 63 | }; 64 | }); 65 | 66 | core_message.addListener("git-clone", checkForDone); 67 | 68 | const tmpDirectory = tmpDir + "/" + randomStr(6); 69 | git.clone(demoRepoUrl, tmpDirectory); 70 | 71 | await donePromise; 72 | 73 | core_message.removeListener("git-clone", checkForDone); 74 | 75 | await createAndMoveProject( 76 | tmpDirectory, 77 | { 78 | container: null, 79 | logger: () => {}, 80 | text: null 81 | }, 82 | "Demo", 83 | demoRepoUrl 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 31 | 32 | FullStacked 33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /editor/index.scss: -------------------------------------------------------------------------------- 1 | /* 2 | All styles follows the official figma design file 3 | https://www.figma.com/design/xb3JBRCvEWpbwGda03T5QQ/Mockups 4 | */ 5 | 6 | // globals 7 | @use "style/colors.scss"; 8 | @use "style/spacing.scss"; 9 | @use "style/list.scss"; 10 | @use "style/winbox.scss"; 11 | 12 | // components 13 | @use "components/top-bar.scss"; 14 | @use "components/view-scrollable.scss"; 15 | @use "../lib/components/snackbar.scss"; 16 | 17 | // views 18 | @use "views/projects/index.scss"; 19 | @use "views/project-settings.scss"; 20 | @use "views/add-project/index.scss" as index2; 21 | @use "views/settings/index.scss" as index3; 22 | @use "views/project/index.scss" as index4; 23 | @use "views/project/git/index.scss" as index5; 24 | @use "views/packages/index.scss" as index6; 25 | @use "views/prompt/index.scss" as index7; 26 | 27 | // deps 28 | @use "../node_modules/@fullstacked/ui/ui.scss"; 29 | @use "../node_modules/@fullstacked/code-editor/editor.scss"; 30 | @use "../node_modules/@fullstacked/file-tree/file-tree.scss"; 31 | 32 | html, 33 | body { 34 | height: 100%; 35 | } 36 | -------------------------------------------------------------------------------- /editor/index.ts: -------------------------------------------------------------------------------- 1 | import "./init"; 2 | import core_message from "../lib/core_message"; 3 | import { deeplink, WindowsAskForAdmin } from "./deeplink"; 4 | import { Demo } from "./demo"; 5 | import config from "./lib/config"; 6 | import { CONFIG_TYPE } from "./types"; 7 | import { updatePackagesView } from "./views/packages"; 8 | import { Projects } from "./views/projects"; 9 | import platform, { Platform } from "../lib/platform"; 10 | import { InitPrompt } from "./views/prompt"; 11 | import { Store } from "./store"; 12 | import { Project } from "./views/project"; 13 | 14 | core_message.addListener("deeplink", deeplink); 15 | 16 | // fix windows scrollbars 17 | if (navigator.userAgent.includes("Windows")) { 18 | const link = document.createElement("link"); 19 | link.rel = "stylesheet"; 20 | link.href = "/windows.css"; 21 | document.head.append(link); 22 | } 23 | 24 | document.querySelector("#splash")?.remove(); 25 | Projects(); 26 | InitPrompt(); 27 | Store.projects.current.subscribe(Project); 28 | 29 | core_message.addListener("package", (dataStr) => { 30 | try { 31 | updatePackagesView(JSON.parse(dataStr)); 32 | } catch (e) { 33 | console.log(dataStr); 34 | } 35 | }); 36 | 37 | const checkProjectsConfigExists = await config.get(CONFIG_TYPE.PROJECTS, true); 38 | if (!checkProjectsConfigExists) { 39 | if (platform === Platform.WINDOWS) { 40 | WindowsAskForAdmin(); 41 | } 42 | 43 | Demo(); 44 | } 45 | -------------------------------------------------------------------------------- /editor/init.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import * as UI from "@fullstacked/ui"; 3 | UI.setIconsDirectory("/icons"); 4 | -------------------------------------------------------------------------------- /editor/lib/config/config.ts: -------------------------------------------------------------------------------- 1 | import { bridge } from "../../../lib/bridge"; 2 | import { serializeArgs } from "../../../lib/bridge/serialization"; 3 | import { CONFIG_DATA_TYPE, CONFIG_TYPE } from "../../types"; 4 | 5 | export function get( 6 | configType: T, 7 | checkExists: boolean = false 8 | ): Promise { 9 | const payload = new Uint8Array([50, ...serializeArgs([configType])]); 10 | 11 | const transformer = ([string]) => { 12 | if (!string) { 13 | return checkExists ? null : {}; 14 | } 15 | 16 | return JSON.parse(string); 17 | }; 18 | 19 | return bridge(payload, transformer); 20 | } 21 | 22 | export function save( 23 | configType: T, 24 | configData: CONFIG_DATA_TYPE[T] 25 | ): Promise { 26 | const payload = new Uint8Array([ 27 | 51, 28 | ...serializeArgs([configType, JSON.stringify(configData)]) 29 | ]); 30 | 31 | return bridge(payload, ([success]) => success); 32 | } 33 | -------------------------------------------------------------------------------- /editor/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | import * as config from "./config"; 2 | export default config; 3 | export * from "./config"; 4 | -------------------------------------------------------------------------------- /editor/lib/core_open.ts: -------------------------------------------------------------------------------- 1 | import { bridge } from "../../lib/bridge"; 2 | import { serializeArgs } from "../../lib/bridge/serialization"; 3 | 4 | // 100 5 | export default function core_open(projectId: string) { 6 | const payload = new Uint8Array([100, ...serializeArgs([projectId])]); 7 | 8 | return bridge(payload); 9 | } 10 | -------------------------------------------------------------------------------- /editor/lib/esbuild/esbuild.ts: -------------------------------------------------------------------------------- 1 | import { bridge } from "../../../lib/bridge"; 2 | import { 3 | deserializeArgs, 4 | getLowestKeyIdAvailable, 5 | serializeArgs 6 | } from "../../../lib/bridge/serialization"; 7 | import { Project } from "../../types"; 8 | import type { Message } from "esbuild"; 9 | import core_message from "../../../lib/core_message"; 10 | import { toByteArray } from "../../../lib/base64"; 11 | 12 | // 55 13 | export function version(): Promise { 14 | const payload = new Uint8Array([55]); 15 | return bridge(payload, ([str]) => str); 16 | } 17 | 18 | let addedListener = false; 19 | const activeBuilds = new Map< 20 | number, 21 | { project: Project; resolve: (buildErrors: Message[]) => void } 22 | >(); 23 | 24 | function buildResponse(buildResult: string) { 25 | const responseData = toByteArray(buildResult); 26 | const [id, errorsStr] = deserializeArgs(responseData); 27 | const activeBuild = activeBuilds.get(id); 28 | 29 | if (!errorsStr) { 30 | return; 31 | } 32 | 33 | const errors = JSON.parse(errorsStr); 34 | const messages = errors?.map(uncapitalizeKeys).map((error) => ({ 35 | ...error, 36 | location: error.location 37 | ? { 38 | ...error.location, 39 | file: error.location.file.includes(activeBuild.project.id) 40 | ? activeBuild.project.id + 41 | error.location.file.split(activeBuild.project.id).pop() 42 | : error.location.file 43 | } 44 | : null 45 | })); 46 | activeBuild.resolve(messages); 47 | 48 | activeBuilds.delete(id); 49 | } 50 | 51 | // 56 52 | export function build(project: Project): Promise { 53 | if (!addedListener) { 54 | core_message.addListener("build", buildResponse); 55 | addedListener = true; 56 | } 57 | 58 | const buildId = getLowestKeyIdAvailable(activeBuilds); 59 | const payload = new Uint8Array([ 60 | 56, 61 | ...serializeArgs([project.id, buildId]) 62 | ]); 63 | 64 | return new Promise((resolve) => { 65 | activeBuilds.set(buildId, { 66 | project, 67 | resolve 68 | }); 69 | bridge(payload); 70 | }); 71 | } 72 | 73 | function isPlainObject(input: any) { 74 | return input && !Array.isArray(input) && typeof input === "object"; 75 | } 76 | 77 | function uncapitalizeKeys(obj: T) { 78 | const final = {}; 79 | for (const [key, value] of Object.entries(obj)) { 80 | final[key.at(0).toLowerCase() + key.slice(1)] = isPlainObject(value) 81 | ? uncapitalizeKeys(value) 82 | : value; 83 | } 84 | return final as T; 85 | } 86 | -------------------------------------------------------------------------------- /editor/lib/esbuild/index.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from "./esbuild"; 2 | export default esbuild; 3 | export * from "./esbuild"; 4 | -------------------------------------------------------------------------------- /editor/lib/esbuild/sass.ts: -------------------------------------------------------------------------------- 1 | import * as sass from "sass"; 2 | import type { Message } from "esbuild"; 3 | import { Project } from "../../types"; 4 | import fs from "../../../lib/fs"; 5 | 6 | export async function buildSASS(project: Project): Promise> { 7 | const writeOutputCSS = async (css: string) => { 8 | const buildDirectory = `${project.id}/.build`; 9 | await fs.mkdir(buildDirectory); 10 | await fs.writeFile(buildDirectory + "/index.css", css); 11 | }; 12 | 13 | const contents = await fs.readdir(project.id); 14 | const entryPointSASS = contents.find( 15 | (item) => item === "index.sass" || item === "index.scss" 16 | ); 17 | 18 | // check for css file and write to output 19 | // esbuild will pick it up and merge with css in js 20 | if (!entryPointSASS) { 21 | const entryPointCSS = contents.find((item) => item === "index.css"); 22 | if (entryPointCSS) { 23 | // TODO: fs.copyFile 24 | await writeOutputCSS( 25 | await fs.readFile(`${project.id}/${entryPointCSS}`, { 26 | encoding: "utf8" 27 | }) 28 | ); 29 | } else { 30 | await writeOutputCSS(""); 31 | } 32 | 33 | return; 34 | } 35 | 36 | const entryData = await fs.readFile(`${project.id}/${entryPointSASS}`, { 37 | encoding: "utf8" 38 | }); 39 | let result: sass.CompileResult; 40 | try { 41 | result = await sass.compileStringAsync(entryData, { 42 | importer: { 43 | load: async (url) => { 44 | const filePath = `${project.id}${url.pathname}`; 45 | const contents = await fs.readFile(filePath, { 46 | encoding: "utf8" 47 | }); 48 | return { 49 | syntax: filePath.endsWith(".sass") 50 | ? "indented" 51 | : filePath.endsWith(".scss") 52 | ? "scss" 53 | : "css", 54 | contents 55 | }; 56 | }, 57 | canonicalize: (path) => new URL(path, window.location.href) 58 | } 59 | }); 60 | } catch (e) { 61 | const error = e as unknown as sass.Exception; 62 | const file = 63 | project.id + (error.span.url?.pathname || "/" + entryPointSASS); 64 | const line = error.span.start.line + 1; 65 | const column = error.span.start.column; 66 | const length = error.span.text.length; 67 | return { 68 | text: error.message, 69 | location: { 70 | file, 71 | line, 72 | column, 73 | length, 74 | namespace: "SASS", 75 | lineText: error.message, 76 | suggestion: "" 77 | } 78 | }; 79 | } 80 | 81 | await writeOutputCSS(result.css); 82 | return null; 83 | } 84 | -------------------------------------------------------------------------------- /editor/lib/fs_sync/fs_sync.ts: -------------------------------------------------------------------------------- 1 | import { fromByteArray } from "../../../lib/base64"; 2 | import { 3 | deserializeArgs, 4 | serializeArgs 5 | } from "../../../lib/bridge/serialization"; 6 | 7 | function syncRequest(method: number, ...args: any[]) { 8 | const request = new XMLHttpRequest(); 9 | const searchParams = new URLSearchParams(); 10 | const payload = new Uint8Array([method, ...serializeArgs(args)]); 11 | searchParams.set("payload", encodeURIComponent(fromByteArray(payload))); 12 | request.open("GET", "/call-sync?" + searchParams.toString(), false); 13 | request.responseType = "arraybuffer"; 14 | request.send(); 15 | 16 | return deserializeArgs(new Uint8Array(request.response)); 17 | } 18 | 19 | // only for WASM 20 | export let cache: Map = null; 21 | export function initCache() { 22 | if (cache) return; 23 | cache = new Map(); 24 | } 25 | 26 | const debug = false; 27 | 28 | export function staticFile(path: string) { 29 | if (debug) { 30 | console.log("staticFile", path); 31 | } 32 | 33 | if (cache) { 34 | return cache.get(path); 35 | } 36 | 37 | const request = new XMLHttpRequest(); 38 | request.open("GET", "/" + path, false); 39 | request.send(); 40 | return request.responseText; 41 | } 42 | 43 | // 2 44 | export function readFile(path: string): string { 45 | if (debug) { 46 | console.log("readFile", path); 47 | } 48 | 49 | if (cache) { 50 | return cache.get(path); 51 | } 52 | 53 | return syncRequest( 54 | 2, 55 | path, 56 | true // encoding == "utf8" 57 | ).at(0); 58 | } 59 | 60 | // 5 61 | export function readdir(path: string, skip: string[]): string[] { 62 | if (debug) { 63 | console.log("readdir", path); 64 | } 65 | 66 | if (cache) { 67 | const items = []; 68 | for (const i of cache.keys()) { 69 | if ( 70 | i.startsWith(path) && 71 | !skip.find((s) => i.startsWith(path + "/" + s)) 72 | ) { 73 | items.push(i.slice(path.length + 1)); 74 | } 75 | } 76 | return items; 77 | } 78 | 79 | return syncRequest( 80 | 5, 81 | path, 82 | true, // recursive 83 | false, // withFileType 84 | ...skip 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /editor/lib/fs_sync/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs_sync from "./fs_sync"; 2 | export default fs_sync; 3 | export * from "./fs_sync"; 4 | -------------------------------------------------------------------------------- /editor/lib/git/index.ts: -------------------------------------------------------------------------------- 1 | import * as git from "./git"; 2 | export default git; 3 | export * from "./git"; 4 | -------------------------------------------------------------------------------- /editor/lib/packages/index.ts: -------------------------------------------------------------------------------- 1 | import * as packages from "./packages"; 2 | export default packages; 3 | export * from "./packages"; 4 | -------------------------------------------------------------------------------- /editor/stack-navigation.ts: -------------------------------------------------------------------------------- 1 | import StackNavigation from "@fullstacked/stack-navigation"; 2 | 3 | export default new StackNavigation(); 4 | -------------------------------------------------------------------------------- /editor/store/editor.ts: -------------------------------------------------------------------------------- 1 | import { createSubscribable } from "."; 2 | 3 | let sidePanelClosed = false; 4 | const sidePanel = createSubscribable(() => sidePanelClosed); 5 | 6 | const codeEditorOpenedFiles = new Set(); 7 | const openedFiles = createSubscribable(() => codeEditorOpenedFiles); 8 | 9 | let codeEditorFocusedFile: string; 10 | const focusedFile = createSubscribable(() => codeEditorFocusedFile); 11 | 12 | export type BuildError = { 13 | file: string; 14 | line: number; 15 | col: number; 16 | length: number; 17 | message: string; 18 | }; 19 | let codeEditorBuildErrors: BuildError[] = []; 20 | const buildErrors = createSubscribable(() => codeEditorBuildErrors); 21 | 22 | export const editor = { 23 | sidePanelClosed: sidePanel.subscription, 24 | setSidePanelClosed, 25 | 26 | codeEditor: { 27 | openedFiles: openedFiles.subscription, 28 | openFile, 29 | closeFile, 30 | closeFilesUnderDirectory, 31 | 32 | focusedFile: focusedFile.subscription, 33 | focusFile, 34 | 35 | clearFiles, 36 | 37 | buildErrors: buildErrors.subscription, 38 | addBuildErrors, 39 | clearAllBuildErrors 40 | } 41 | }; 42 | 43 | function setSidePanelClosed(closed: boolean) { 44 | sidePanelClosed = closed; 45 | sidePanel.notify(); 46 | } 47 | 48 | function openFile(path: string) { 49 | codeEditorOpenedFiles.add(path); 50 | openedFiles.notify(); 51 | } 52 | 53 | function closeFile(path: string) { 54 | codeEditorOpenedFiles.delete(path); 55 | if (path === codeEditorFocusedFile) { 56 | if (codeEditorOpenedFiles.size > 0) { 57 | codeEditorFocusedFile = Array.from(codeEditorOpenedFiles).at(-1); 58 | } else { 59 | codeEditorFocusedFile = null; 60 | } 61 | focusedFile.notify(); 62 | } 63 | openedFiles.notify(); 64 | } 65 | 66 | function closeFilesUnderDirectory(path: string) { 67 | for (const openedFile of codeEditorOpenedFiles.values()) { 68 | if (openedFile.startsWith(path)) { 69 | codeEditorOpenedFiles.delete(openedFile); 70 | } 71 | } 72 | openedFiles.notify(); 73 | } 74 | 75 | function focusFile(path: string) { 76 | codeEditorFocusedFile = path; 77 | focusedFile.notify(); 78 | } 79 | 80 | function clearFiles() { 81 | codeEditorOpenedFiles.clear(); 82 | codeEditorFocusedFile = null; 83 | openedFiles.notify(); 84 | focusedFile.notify(); 85 | } 86 | 87 | function addBuildErrors(errors: BuildError[]) { 88 | codeEditorBuildErrors.push(...errors); 89 | buildErrors.notify(); 90 | } 91 | 92 | function clearAllBuildErrors() { 93 | codeEditorBuildErrors = []; 94 | buildErrors.notify(); 95 | } 96 | -------------------------------------------------------------------------------- /editor/store/index.ts: -------------------------------------------------------------------------------- 1 | import { editor } from "./editor"; 2 | import { projects } from "./projects"; 3 | import { preferences } from "./preferences"; 4 | 5 | export const Store = { 6 | preferences, 7 | projects, 8 | editor 9 | }; 10 | 11 | export function createSequential>( 12 | fn: (...args: T) => R 13 | ) { 14 | let toRun: { 15 | args: T; 16 | fn: (...args: T) => R; 17 | resolve: (args: Awaited) => void; 18 | }[] = []; 19 | 20 | let lock = false; 21 | 22 | const execute = async () => { 23 | if (lock) return; 24 | 25 | lock = true; 26 | 27 | while (toRun.length) { 28 | const toExecute = toRun.shift(); 29 | const result = await toExecute.fn(...toExecute.args); 30 | toExecute.resolve(result); 31 | } 32 | 33 | lock = false; 34 | }; 35 | 36 | return (...args: T) => { 37 | const promise = new Promise>((resolve) => { 38 | toRun.push({ 39 | args, 40 | fn, 41 | resolve 42 | }); 43 | }); 44 | 45 | execute(); 46 | 47 | return promise; 48 | }; 49 | } 50 | 51 | export function createSubscribable( 52 | getter: () => T, 53 | placeolderValue?: Awaited 54 | ): { 55 | notify: () => void; 56 | subscription: { 57 | check: () => Awaited; 58 | subscribe: (onUpdate: (value: Awaited) => void) => void; 59 | unsubscribe: (onUpdate: (value: Awaited) => void) => void; 60 | }; 61 | } { 62 | const subscribers = new Set<(value: Awaited) => void>(); 63 | 64 | let value: Awaited = placeolderValue; 65 | 66 | const notifySubscribers = (updatedValue: Awaited | undefined) => { 67 | value = updatedValue; 68 | subscribers.forEach((subscriber) => subscriber(value)); 69 | }; 70 | 71 | const notify = () => { 72 | const maybePromise = getter(); 73 | 74 | if (maybePromise instanceof Promise) { 75 | maybePromise.then(notifySubscribers); 76 | } else { 77 | notifySubscribers(maybePromise as Awaited); 78 | } 79 | }; 80 | 81 | const subscribe = (onUpdate: (value: Awaited) => void) => { 82 | subscribers.add(onUpdate); 83 | onUpdate(value); 84 | }; 85 | 86 | const unsubscribe = (onUpdate: (value: Awaited) => void) => { 87 | subscribers.delete(onUpdate); 88 | }; 89 | 90 | const initialValue = getter(); 91 | if (initialValue instanceof Promise) { 92 | initialValue.then(notifySubscribers); 93 | } else { 94 | value = initialValue as Awaited; 95 | } 96 | 97 | return { 98 | notify, 99 | subscription: { 100 | check: () => value, 101 | subscribe, 102 | unsubscribe 103 | } 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /editor/store/preferences.ts: -------------------------------------------------------------------------------- 1 | import { createSubscribable } from "."; 2 | import config from "../lib/config"; 3 | import { CONFIG_TYPE } from "../types"; 4 | 5 | const isUserMode = createSubscribable(getUserMode, false); 6 | 7 | export const preferences = { 8 | setUserMode, 9 | isUserMode: isUserMode.subscription 10 | }; 11 | 12 | let userMode: boolean; 13 | let userModePromise: Promise; 14 | async function getUserMode() { 15 | if (typeof userMode != "boolean") { 16 | if (!userModePromise) { 17 | userModePromise = new Promise(async (resolve) => { 18 | const c = await config.get(CONFIG_TYPE.GENERAL); 19 | userMode = c?.userMode || false; 20 | resolve(); 21 | }); 22 | } 23 | await userModePromise; 24 | } 25 | 26 | return userMode; 27 | } 28 | 29 | async function setUserMode(um: boolean) { 30 | userMode = um; 31 | const c = await config.get(CONFIG_TYPE.GENERAL); 32 | c.userMode = userMode; 33 | await config.save(CONFIG_TYPE.GENERAL, c); 34 | isUserMode.notify(); 35 | } 36 | -------------------------------------------------------------------------------- /editor/style/colors.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/colors.scss"; 2 | 3 | html, 4 | body { 5 | background-color: colors.$blue-dark; 6 | color: colors.$light; 7 | } 8 | -------------------------------------------------------------------------------- /editor/style/list.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | -------------------------------------------------------------------------------- /editor/style/spacing.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | width: 100%; 7 | } 8 | 9 | .view, 10 | .view-scrollable { 11 | padding: spacing.$small spacing.$medium spacing.$medium; 12 | } 13 | -------------------------------------------------------------------------------- /editor/style/winbox.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/colors.scss"; 2 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 3 | 4 | .winbox { 5 | border-radius: spacing.$small; 6 | background-color: colors.$gray-dark; 7 | overflow: hidden; 8 | } 9 | -------------------------------------------------------------------------------- /editor/style/windows.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/colors.scss"; 2 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 3 | 4 | ::-webkit-scrollbar { 5 | width: 8px; 6 | height: 5px; 7 | background-color: colors.opacity(colors.$gray-dark, 0); 8 | } 9 | 10 | ::-webkit-scrollbar:hover { 11 | background-color: colors.opacity(colors.$gray-dark, 50); 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | background: colors.opacity(colors.$gray, 50); 16 | border-radius: 10px; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb:active { 20 | background: colors.opacity(colors.$gray, 80); 21 | border-radius: 10px; 22 | } 23 | 24 | ::-webkit-scrollbar-corner { 25 | background-color: colors.opacity(colors.$gray-dark, 0); 26 | } 27 | 28 | .win-admin-dialog { 29 | display: flex; 30 | flex-direction: column; 31 | 32 | h1 { 33 | margin-bottom: spacing.$small; 34 | } 35 | 36 | p { 37 | margin-bottom: spacing.$x-small; 38 | } 39 | 40 | button { 41 | align-self: flex-end; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /editor/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum CONFIG_TYPE { 2 | GENERAL = "general", 3 | PROJECTS = "projects", 4 | GIT = "git", 5 | AGENT = "agent" 6 | // CONNECTIVITY = "connectivity" 7 | } 8 | 9 | export type CONFIG_DATA_TYPE = { 10 | [CONFIG_TYPE.GENERAL]: { 11 | userMode: boolean; 12 | }; 13 | [CONFIG_TYPE.PROJECTS]: { 14 | projects: Project[]; 15 | }; 16 | [CONFIG_TYPE.GIT]: GitAuths; 17 | 18 | [CONFIG_TYPE.AGENT]: any; 19 | // [CONFIG_TYPE.CONNECTIVITY]: Connectivity; 20 | }; 21 | 22 | export type Project = { 23 | title: string; 24 | id: string; 25 | createdDate: number; 26 | location?: string; 27 | gitRepository?: { 28 | url: string; 29 | name?: string; 30 | email?: string; 31 | merging?: string; 32 | }; 33 | }; 34 | 35 | export type GitAuths = { 36 | [hostname: string]: { 37 | username: string; 38 | password?: string; 39 | email?: string; 40 | }; 41 | }; 42 | 43 | // export type Connectivity = { 44 | // me: Peer; 45 | // autoConnect: boolean; 46 | // defaultNetworkInterface: string; 47 | // webAddresses: WebAddress[]; 48 | // peersTrusted: PeerTrusted[]; 49 | // }; 50 | -------------------------------------------------------------------------------- /editor/views/add-project/create-empty.ts: -------------------------------------------------------------------------------- 1 | import slugify from "slugify"; 2 | import { TopBar } from "../../components/top-bar"; 3 | import { Store } from "../../store"; 4 | import stackNavigation from "../../stack-navigation"; 5 | import { BG_COLOR } from "../../constants"; 6 | import fs from "../../../lib/fs"; 7 | import { Button, InputText } from "@fullstacked/ui"; 8 | 9 | export function CreateEmpty() { 10 | const container = document.createElement("div"); 11 | container.classList.add("view", "create-form"); 12 | 13 | const topBar = TopBar({ 14 | title: "Create empty project" 15 | }); 16 | 17 | container.append(topBar); 18 | 19 | const form = document.createElement("form"); 20 | 21 | const inputTitle = InputText({ 22 | label: "Title" 23 | }); 24 | const inputIdentifier = InputText({ 25 | label: "Identifier" 26 | }); 27 | 28 | inputTitle.input.onblur = () => { 29 | if (!inputIdentifier.input.value) { 30 | inputIdentifier.input.value = slugify(inputTitle.input.value, { 31 | lower: true 32 | }); 33 | } 34 | }; 35 | 36 | const createButton = Button({ 37 | text: "Create" 38 | }); 39 | 40 | form.onsubmit = (e) => { 41 | e.preventDefault(); 42 | createButton.disabled = true; 43 | 44 | let id = inputIdentifier.input.value 45 | ? slugify(inputIdentifier.input.value, { lower: true }) 46 | : slugify(inputTitle.input.value, { lower: true }); 47 | id = id || "no-identifier"; 48 | 49 | const title = inputTitle.input.value || "Empty Project"; 50 | 51 | Promise.all([fs.mkdir(id), Store.projects.create({ title, id })]).then( 52 | () => stackNavigation.back() 53 | ); 54 | }; 55 | 56 | form.append(inputTitle.container, inputIdentifier.container, createButton); 57 | 58 | container.append(form); 59 | 60 | setTimeout(() => inputTitle.input.focus(), 1); 61 | 62 | stackNavigation.navigate(container, { 63 | bgColor: BG_COLOR 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /editor/views/add-project/index.scss: -------------------------------------------------------------------------------- 1 | @use "../../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | @use "../../../node_modules/@fullstacked/ui/values/colors.scss"; 3 | 4 | #add-project { 5 | padding-bottom: spacing.$small; 6 | 7 | .buttons { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | gap: spacing.$large; 12 | padding-top: spacing.$large; 13 | } 14 | } 15 | 16 | .create-form { 17 | min-height: 100%; 18 | display: flex; 19 | flex-direction: column; 20 | 21 | .top-bar { 22 | padding-bottom: spacing.$small; 23 | } 24 | 25 | form { 26 | padding-top: spacing.$medium; 27 | padding-bottom: spacing.$medium; 28 | 29 | display: flex; 30 | flex-direction: column; 31 | gap: spacing.$small; 32 | 33 | width: 100%; 34 | max-width: spacing.$max-width; 35 | 36 | margin: 0 auto; 37 | 38 | button { 39 | align-self: flex-end; 40 | } 41 | } 42 | } 43 | 44 | .create-loader { 45 | display: flex; 46 | flex-direction: column; 47 | align-items: center; 48 | justify-content: center; 49 | text-align: center; 50 | gap: spacing.$medium; 51 | padding-bottom: spacing.$medium; 52 | .loader { 53 | width: 60px; 54 | } 55 | } 56 | 57 | .create-terminal { 58 | background-color: colors.$dark; 59 | border: 1px solid colors.$gray; 60 | color: colors.$light; 61 | overflow: auto; 62 | height: 100%; 63 | border-radius: spacing.$x-small; 64 | min-height: 250px; 65 | flex: 1; 66 | > pre { 67 | padding: spacing.$medium; 68 | min-height: 100%; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /editor/views/add-project/index.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "@fullstacked/ui"; 2 | import { TopBar } from "../../components/top-bar"; 3 | import { BG_COLOR, IMPORT_ZIP_ID } from "../../constants"; 4 | import stackNavigation from "../../stack-navigation"; 5 | import { Store } from "../../store"; 6 | import { CloneGit } from "./clone-git"; 7 | import { CreateEmpty } from "./create-empty"; 8 | import { ImportZip } from "./import-zip"; 9 | 10 | export function AddProject() { 11 | const container = document.createElement("div"); 12 | container.id = "add-project"; 13 | container.classList.add("view"); 14 | 15 | const topBar = TopBar({ 16 | title: "Add Project" 17 | }); 18 | 19 | container.append(topBar); 20 | 21 | const buttonsContainer = document.createElement("div"); 22 | buttonsContainer.classList.add("buttons"); 23 | 24 | const cloneGitButton = Button({ 25 | text: "Clone git repository", 26 | iconLeft: "Git" 27 | }); 28 | cloneGitButton.onclick = () => CloneGit(); 29 | // cloneGitButton.disabled = platform === Platform.WASM; 30 | 31 | const importZipButton = Button({ 32 | text: "Import zip", 33 | iconLeft: "Archive" 34 | }); 35 | importZipButton.id = IMPORT_ZIP_ID; 36 | importZipButton.onclick = ImportZip; 37 | 38 | const createEmptyButton = Button({ 39 | text: "Create empty project", 40 | iconLeft: "Glitter" 41 | }); 42 | createEmptyButton.onclick = CreateEmpty; 43 | 44 | buttonsContainer.append(cloneGitButton, importZipButton, createEmptyButton); 45 | container.append(buttonsContainer); 46 | 47 | // on project list update (most probably new project created) 48 | // go back 49 | const goBackOnNewProject = () => stackNavigation.back(); 50 | Store.projects.list.subscribe(goBackOnNewProject); 51 | 52 | stackNavigation.navigate(container, { 53 | bgColor: BG_COLOR, 54 | onDestroy: () => { 55 | Store.projects.list.unsubscribe(goBackOnNewProject); 56 | } 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /editor/views/packages/index.scss: -------------------------------------------------------------------------------- 1 | @use "../../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | @use "../../../node_modules/@fullstacked/ui/values/colors.scss"; 3 | @use "../../../node_modules/@fullstacked/ui/values/typography.scss"; 4 | 5 | .packages-view { 6 | h3 { 7 | padding-bottom: spacing.$small; 8 | } 9 | 10 | ul { 11 | display: flex; 12 | flex-direction: column; 13 | gap: spacing.$small; 14 | 15 | li { 16 | background-color: colors.$gray-dark; 17 | border: 1px solid colors.$gray; 18 | display: flex; 19 | align-items: center; 20 | justify-content: space-between; 21 | position: relative; 22 | padding: spacing.$small; 23 | border-radius: spacing.$x-small; 24 | overflow: hidden; 25 | 26 | > div:nth-child(2) { 27 | font-size: typography.$small; 28 | } 29 | 30 | .progress-bar { 31 | position: absolute; 32 | left: 0; 33 | bottom: 0; 34 | width: 0; 35 | height: spacing.$x-small; 36 | background-color: colors.$blue; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /editor/views/project-settings.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | 3 | .project-settings { 4 | form { 5 | width: 100%; 6 | max-width: spacing.$max-width; 7 | margin: 0 auto; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | gap: spacing.$small; 12 | padding-top: spacing.$medium; 13 | 14 | > button { 15 | align-self: flex-end; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /editor/views/project-settings.ts: -------------------------------------------------------------------------------- 1 | import { TopBar } from "../components/top-bar"; 2 | import { ViewScrollable } from "../components/view-scrollable"; 3 | import slugify from "slugify"; 4 | import stackNavigation from "../stack-navigation"; 5 | import { BG_COLOR } from "../constants"; 6 | import { Project } from "../types"; 7 | import { Store } from "../store"; 8 | import { Button, InputText } from "@fullstacked/ui"; 9 | 10 | export function ProjectSettings(project: Project) { 11 | const { container, scrollable } = ViewScrollable(); 12 | container.classList.add("project-settings"); 13 | 14 | container.prepend( 15 | TopBar({ 16 | title: "Project Settings" 17 | }) 18 | ); 19 | 20 | const form = document.createElement("form"); 21 | 22 | const titleInput = InputText({ 23 | label: "Title" 24 | }); 25 | titleInput.input.value = project.title; 26 | const identifierInput = InputText({ 27 | label: "Identifier" 28 | }); 29 | identifierInput.input.value = project.id; 30 | identifierInput.input.onblur = () => { 31 | identifierInput.input.value = slugify(identifierInput.input.value, { 32 | lower: true 33 | }); 34 | }; 35 | 36 | const updateButton = Button({ 37 | text: "Update" 38 | }); 39 | 40 | form.append(titleInput.container, identifierInput.container, updateButton); 41 | 42 | form.onsubmit = (e) => { 43 | e.preventDefault(); 44 | 45 | updateButton.disabled = true; 46 | identifierInput.input.value = slugify(identifierInput.input.value, { 47 | lower: true 48 | }); 49 | 50 | const updatedProject = { 51 | ...project 52 | }; 53 | 54 | updatedProject.title = titleInput.input.value; 55 | updatedProject.id = identifierInput.input.value; 56 | 57 | if ( 58 | updatedProject.title === project.title && 59 | updatedProject.id === project.id 60 | ) { 61 | stackNavigation.back(); 62 | return; 63 | } 64 | 65 | Store.projects 66 | .update(project, updatedProject) 67 | .then(() => stackNavigation.back()); 68 | }; 69 | 70 | scrollable.append(form); 71 | 72 | stackNavigation.navigate(container, { 73 | bgColor: BG_COLOR 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /editor/views/project/dev-icons.scss: -------------------------------------------------------------------------------- 1 | .dev-icon { 2 | &.typescript::before { 3 | content: "\E099"; 4 | color: #519aba; 5 | } 6 | 7 | &.javascript::before { 8 | content: "\E051"; 9 | color: #cbcb41; 10 | } 11 | 12 | &.sass::before { 13 | content: "\E084"; 14 | color: #f55385; 15 | } 16 | 17 | &.css::before { 18 | content: "\E01D"; 19 | color: #519aba; 20 | } 21 | 22 | &.json::before { 23 | content: "\E055"; 24 | color: #cbcb41; 25 | } 26 | 27 | &.markdown::before { 28 | content: "\E060"; 29 | color: #519aba; 30 | } 31 | 32 | &.liquid::before { 33 | content: "\E05B"; 34 | color: #8dc149; 35 | } 36 | 37 | &.html::before { 38 | content: "\E048"; 39 | color: #e37933; 40 | } 41 | 42 | &.image::before { 43 | content: "\E04C"; 44 | color: #a074c4; 45 | } 46 | 47 | &.svg::before { 48 | content: "\E04C"; 49 | color: #a074c4; 50 | } 51 | 52 | &.react::before { 53 | content: "\E07D"; 54 | color: #519aba; 55 | } 56 | 57 | &.npm::before { 58 | content: "\E067"; 59 | color: #cc3e44; 60 | } 61 | 62 | &.default::before { 63 | content: "\E023"; 64 | color: #d4d7d6; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /editor/views/project/file-event.ts: -------------------------------------------------------------------------------- 1 | export enum FileEventType { 2 | UNKNOWN = 0, 3 | CREATED = 1, 4 | MODIFIED = 2, 5 | RENAME = 3, 6 | DELETED = 4 7 | } 8 | 9 | export type FileEvent = { 10 | isFile: boolean; 11 | origin: string; 12 | paths: string[]; 13 | type: FileEventType; 14 | }; 15 | -------------------------------------------------------------------------------- /editor/views/project/git/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { GitHubDeviceFlow } from "./github"; 2 | import { createElement } from "../../../../components/element"; 3 | import { CONFIG_TYPE } from "../../../../types"; 4 | import config from "../../../../lib/config"; 5 | import { Button, Dialog, InputText } from "@fullstacked/ui"; 6 | 7 | export function GitAuth(hostname: string): Promise { 8 | if (hostname === "github.com") { 9 | return GitHubDeviceFlow(); 10 | } 11 | 12 | const container = createElement("div"); 13 | container.classList.add("git-auth"); 14 | 15 | container.innerHTML = `

Git Authentication

16 |

Authenticate for ${hostname}

`; 17 | 18 | const form = document.createElement("form"); 19 | 20 | const usernameInput = InputText({ 21 | label: "Username" 22 | }); 23 | const emailInput = InputText({ 24 | label: "Email (optional)" 25 | }); 26 | emailInput.input.type = "email"; 27 | const passwordInput = InputText({ 28 | label: "Password" 29 | }); 30 | passwordInput.input.type = "password"; 31 | 32 | const buttons = document.createElement("div"); 33 | 34 | const cancelButton = Button({ 35 | text: "Cancel", 36 | style: "text" 37 | }); 38 | cancelButton.type = "button"; 39 | 40 | const authButton = Button({ 41 | text: "Authenticate" 42 | }); 43 | buttons.append(cancelButton, authButton); 44 | form.append( 45 | usernameInput.container, 46 | emailInput.container, 47 | passwordInput.container, 48 | buttons 49 | ); 50 | 51 | container.append(form); 52 | 53 | const { remove } = Dialog(container); 54 | 55 | return new Promise((resolve) => { 56 | cancelButton.onclick = () => { 57 | resolve(false); 58 | remove(); 59 | }; 60 | 61 | form.onsubmit = async (e) => { 62 | e.preventDefault(); 63 | 64 | authButton.disabled = true; 65 | 66 | const gitAuthConfigs = await config.get(CONFIG_TYPE.GIT); 67 | gitAuthConfigs[hostname] = { 68 | username: usernameInput.input.value, 69 | email: emailInput.input.value, 70 | password: passwordInput.input.value 71 | }; 72 | await config.save(CONFIG_TYPE.GIT, gitAuthConfigs); 73 | 74 | resolve(true); 75 | remove(); 76 | }; 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /editor/views/projects/index.scss: -------------------------------------------------------------------------------- 1 | @use "../../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | @use "../../../node_modules/@fullstacked/ui/values/colors.scss"; 3 | @use "../../../node_modules/@fullstacked/ui/values/breakpoints.scss"; 4 | 5 | #projects-view { 6 | .top-bar { 7 | padding-bottom: spacing.$small; 8 | } 9 | .search-and-add { 10 | padding-top: spacing.$small; 11 | 12 | width: 100%; 13 | display: flex; 14 | align-items: flex-end; 15 | justify-content: space-between; 16 | gap: spacing.$small; 17 | 18 | padding-bottom: spacing.$medium; 19 | 20 | > div:first-child { 21 | flex: 1; 22 | max-width: spacing.$max-width; 23 | } 24 | } 25 | 26 | .projects-list { 27 | display: grid; 28 | gap: spacing.$medium; 29 | grid-template-columns: repeat(4, 1fr); 30 | 31 | @media (max-width: breakpoints.$x-large) { 32 | grid-template-columns: repeat(3, 1fr); 33 | } 34 | 35 | @media (max-width: breakpoints.$large) { 36 | grid-template-columns: repeat(2, 1fr); 37 | } 38 | 39 | @media (max-width: breakpoints.$small-med) { 40 | grid-template-columns: repeat(1, 1fr); 41 | } 42 | 43 | .project-tile { 44 | position: relative; 45 | 46 | cursor: pointer; 47 | 48 | background-color: colors.opacity(colors.$light, 15); 49 | width: 100%; 50 | aspect-ratio: 39 / 22; 51 | 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | 56 | text-align: center; 57 | overflow: hidden; 58 | 59 | padding: spacing.$small; 60 | 61 | &.loading { 62 | background-color: colors.opacity(colors.$light, 25); 63 | } 64 | 65 | > .title-id { 66 | width: 100%; 67 | display: flex; 68 | flex-direction: column; 69 | align-items: center; 70 | gap: spacing.$x-small; 71 | 72 | > h2 { 73 | width: 100%; 74 | text-overflow: ellipsis; 75 | overflow: hidden; 76 | direction: rtl; 77 | white-space: nowrap; 78 | } 79 | } 80 | 81 | > button { 82 | position: absolute; 83 | color: white; 84 | bottom: 0; 85 | right: spacing.$x-small; 86 | } 87 | 88 | .options-popover { 89 | padding: spacing.$x-small; 90 | } 91 | 92 | .loader { 93 | position: absolute; 94 | top: spacing.$x-small; 95 | left: spacing.$x-small; 96 | height: 24px; 97 | width: 24px; 98 | } 99 | } 100 | } 101 | 102 | .peers-widget { 103 | display: flex; 104 | font-weight: bold; 105 | justify-content: flex-end; 106 | text-align: right; 107 | align-items: center; 108 | color: colors.$blue; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /editor/views/projects/index.ts: -------------------------------------------------------------------------------- 1 | import { ViewScrollable } from "../../components/view-scrollable"; 2 | import { 3 | BG_COLOR, 4 | PROJECTS_TITLE, 5 | PROJECTS_VIEW_ID, 6 | SETTINGS_BUTTON_ID 7 | } from "../../constants"; 8 | import stackNavigation from "../../stack-navigation"; 9 | import { List } from "./list"; 10 | import { SearchAdd } from "./search-add"; 11 | import { TopBar as TopBarComponent } from "../../components/top-bar"; 12 | import { PeersWidget } from "./peers-widget"; 13 | import { Settings } from "../settings"; 14 | import { Button } from "@fullstacked/ui"; 15 | 16 | export function Projects() { 17 | const { container, scrollable } = ViewScrollable(); 18 | container.id = PROJECTS_VIEW_ID; 19 | 20 | const topBar = TopBar(); 21 | container.prepend(topBar); 22 | 23 | const list = List(); 24 | 25 | scrollable.append(SearchAdd(), list); 26 | 27 | stackNavigation.navigate(container, { 28 | bgColor: BG_COLOR, 29 | onDestroy: () => { 30 | topBar.destroy(); 31 | list.destroy(); 32 | } 33 | }); 34 | } 35 | 36 | function TopBar() { 37 | const settings = Button({ 38 | style: "icon-large", 39 | iconLeft: "Settings" 40 | }); 41 | settings.id = SETTINGS_BUTTON_ID; 42 | settings.onclick = Settings; 43 | 44 | const peersWidget = PeersWidget(); 45 | 46 | const topBar = TopBarComponent({ 47 | noBack: true, 48 | title: PROJECTS_TITLE, 49 | actions: [peersWidget, settings] 50 | }); 51 | 52 | topBar.ondestroy = peersWidget.destroy; 53 | 54 | return topBar; 55 | } 56 | -------------------------------------------------------------------------------- /editor/views/projects/peers-widget.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "../../components/element"; 2 | 3 | export function PeersWidget() { 4 | return createElement("div"); 5 | } 6 | -------------------------------------------------------------------------------- /editor/views/projects/search-add.ts: -------------------------------------------------------------------------------- 1 | import { Button, InputText } from "@fullstacked/ui"; 2 | import { NEW_PROJECT_ID } from "../../constants"; 3 | import { AddProject } from "../add-project"; 4 | import { filterProjects } from "./list"; 5 | 6 | export function SearchAdd() { 7 | const container = document.createElement("div"); 8 | container.classList.add("search-and-add"); 9 | 10 | const search = Search(); 11 | const add = Add(); 12 | container.append(search, add); 13 | return container; 14 | } 15 | 16 | function Search() { 17 | const inputSearch = InputText({ 18 | label: "Search" 19 | }); 20 | 21 | inputSearch.input.onkeyup = () => { 22 | filterProjects(inputSearch.input.value); 23 | }; 24 | 25 | return inputSearch.container; 26 | } 27 | 28 | function Add() { 29 | const addButton = Button({ 30 | style: "icon-large", 31 | iconLeft: "Plus" 32 | }); 33 | addButton.id = NEW_PROJECT_ID; 34 | 35 | addButton.onclick = AddProject; 36 | 37 | return addButton; 38 | } 39 | -------------------------------------------------------------------------------- /editor/views/prompt/index.scss: -------------------------------------------------------------------------------- 1 | @use "../../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | 3 | .prompt-container { 4 | display: flex; 5 | flex-direction: column; 6 | gap: spacing.$small; 7 | > button { 8 | align-self: flex-end; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /editor/views/settings/index.ts: -------------------------------------------------------------------------------- 1 | import { TopBar } from "../../components/top-bar"; 2 | import { ViewScrollable } from "../../components/view-scrollable"; 3 | import stackNavigation from "../../stack-navigation"; 4 | import { BG_COLOR, SETTINGS_VIEW_ID } from "../../constants"; 5 | import { Version } from "./version"; 6 | import { GitAuthentications } from "./git-authentications"; 7 | import { createElement } from "../../components/element"; 8 | import { Store } from "../../store"; 9 | import { InputSwitch } from "@fullstacked/ui"; 10 | import { codeEditor } from "../../code-editor"; 11 | 12 | export function Settings() { 13 | const { container, scrollable } = ViewScrollable(); 14 | container.id = SETTINGS_VIEW_ID; 15 | container.classList.add("view"); 16 | 17 | const topBar = TopBar({ 18 | title: "Settings" 19 | }); 20 | 21 | container.prepend(topBar); 22 | 23 | const userMode = UserMode(); 24 | 25 | scrollable.append( 26 | userMode, 27 | AgentProvider(), 28 | GitAuthentications(), 29 | Version() 30 | ); 31 | 32 | stackNavigation.navigate(container, { 33 | bgColor: BG_COLOR, 34 | onDestroy: userMode.destroy 35 | }); 36 | } 37 | 38 | function UserMode() { 39 | const container = createElement("div"); 40 | container.classList.add("user-mode"); 41 | 42 | const top = document.createElement("div"); 43 | top.innerHTML = `

User Mode

`; 44 | 45 | const inputSwitch = InputSwitch(); 46 | top.append(inputSwitch.container); 47 | 48 | const p = document.createElement("p"); 49 | p.innerText = `Simpler interface, removes all developer-related elements. 50 | Projects start faster, builds only when needed.`; 51 | 52 | container.append(top, p); 53 | 54 | const cb = (userMode: boolean) => { 55 | inputSwitch.input.checked = userMode; 56 | }; 57 | Store.preferences.isUserMode.subscribe(cb); 58 | container.ondestroy = () => { 59 | Store.preferences.isUserMode.unsubscribe(cb); 60 | }; 61 | inputSwitch.input.onchange = () => { 62 | Store.preferences.setUserMode(inputSwitch.input.checked); 63 | }; 64 | 65 | return container; 66 | } 67 | 68 | function AgentProvider() { 69 | const container = document.createElement("div"); 70 | container.classList.add("agent-provider-config"); 71 | container.innerHTML = `

Configure Agent Providers

`; 72 | 73 | container.append(codeEditor.agentConfigurator); 74 | 75 | return container; 76 | } 77 | -------------------------------------------------------------------------------- /lib/archive/index.ts: -------------------------------------------------------------------------------- 1 | import * as archive from "./archive"; 2 | 3 | export default archive; 4 | export * from "./archive"; 5 | -------------------------------------------------------------------------------- /lib/bridge/index.ts: -------------------------------------------------------------------------------- 1 | import "../core_message"; 2 | import platform, { Platform } from "../platform"; 3 | import { BridgeAndroid } from "./platform/android"; 4 | import { BridgeApple, initRespondApple } from "./platform/apple"; 5 | import { BridgeLinux, initRespondLinux } from "./platform/linux"; 6 | import { BridgeNode, initCallbackNode } from "./platform/node"; 7 | import { BridgeWasm } from "./platform/wasm"; 8 | import { BridgeWindows, initRespondWindows } from "./platform/windows"; 9 | 10 | export type Bridge = ( 11 | payload: Uint8Array, 12 | transformer?: (args: any) => any 13 | ) => Promise; 14 | 15 | export let bridge: Bridge; 16 | 17 | switch (platform) { 18 | case Platform.NODE: 19 | bridge = BridgeNode; 20 | initCallbackNode(); 21 | break; 22 | case Platform.APPLE: 23 | bridge = BridgeApple; 24 | initRespondApple(); 25 | break; 26 | case Platform.ANDROID: 27 | bridge = BridgeAndroid; 28 | break; 29 | case Platform.WASM: 30 | bridge = BridgeWasm; 31 | break; 32 | case Platform.WINDOWS: 33 | bridge = BridgeWindows; 34 | initRespondWindows(); 35 | break; 36 | case Platform.LINUX: 37 | bridge = BridgeLinux; 38 | initRespondLinux(); 39 | break; 40 | case Platform.DOCKER: 41 | console.log("Bridge not yet implemented"); 42 | } 43 | 44 | console.log("FullStacked"); 45 | bridge(new Uint8Array([0])); 46 | -------------------------------------------------------------------------------- /lib/bridge/platform/android.ts: -------------------------------------------------------------------------------- 1 | import { Bridge } from ".."; 2 | import { fromByteArray, toByteArray } from "../../base64"; 3 | import { deserializeArgs } from "../serialization"; 4 | 5 | export const BridgeAndroid: Bridge = async ( 6 | payload: Uint8Array, 7 | transformer?: (responseArgs: any[]) => any 8 | ) => { 9 | const base64 = fromByteArray(payload); 10 | const response = toByteArray(globalThis.android.bridge(base64)); 11 | const args = deserializeArgs(response); 12 | 13 | if (transformer) { 14 | return transformer(args); 15 | } 16 | 17 | return args; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/bridge/platform/apple.ts: -------------------------------------------------------------------------------- 1 | import { Bridge } from ".."; 2 | import { fromByteArray, toByteArray } from "../../base64"; 3 | import { 4 | bytesToNumber, 5 | deserializeArgs, 6 | getLowestKeyIdAvailable, 7 | numberTo4Bytes 8 | } from "../serialization"; 9 | 10 | const requests = new Map void>(); 11 | 12 | export const BridgeApple: Bridge = ( 13 | payload: Uint8Array, 14 | transformer?: (responseArgs: any[]) => any 15 | ) => { 16 | const requestId = getLowestKeyIdAvailable(requests); 17 | 18 | const base64 = fromByteArray( 19 | new Uint8Array([...numberTo4Bytes(requestId), ...payload]) 20 | ); 21 | 22 | return new Promise((resolve, reject) => { 23 | requests.set(requestId, (data) => { 24 | try { 25 | const args = deserializeArgs(data); 26 | if (transformer) { 27 | return resolve(transformer(args)); 28 | } 29 | resolve(args); 30 | } catch (e) { 31 | reject(e); 32 | } 33 | }); 34 | globalThis.webkit.messageHandlers.bridge.postMessage(base64); 35 | }); 36 | }; 37 | 38 | export function initRespondApple() { 39 | globalThis.respond = (base64: string) => { 40 | const data = toByteArray(base64); 41 | const id = bytesToNumber(data.slice(0, 4)); 42 | const resolver = requests.get(id); 43 | resolver(data.slice(4)); 44 | requests.delete(id); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /lib/bridge/platform/linux.ts: -------------------------------------------------------------------------------- 1 | import { Bridge } from ".."; 2 | import { fromByteArray, toByteArray } from "../../base64"; 3 | import { 4 | bytesToNumber, 5 | deserializeArgs, 6 | getLowestKeyIdAvailable, 7 | numberTo4Bytes 8 | } from "../serialization"; 9 | 10 | const requests = new Map void>(); 11 | 12 | export const BridgeLinux: Bridge = ( 13 | payload: Uint8Array, 14 | transformer?: (responseArgs: any[]) => any 15 | ) => { 16 | const requestId = getLowestKeyIdAvailable(requests); 17 | 18 | const base64 = fromByteArray( 19 | new Uint8Array([...numberTo4Bytes(requestId), ...payload]) 20 | ); 21 | 22 | return new Promise((resolve, reject) => { 23 | requests.set(requestId, (data) => { 24 | try { 25 | const args = deserializeArgs(data); 26 | if (transformer) { 27 | return resolve(transformer(args)); 28 | } 29 | resolve(args); 30 | } catch (e) { 31 | reject(e); 32 | } 33 | }); 34 | globalThis.webkit.messageHandlers.bridge.postMessage(base64); 35 | }); 36 | }; 37 | 38 | export function initRespondLinux() { 39 | globalThis.respond = (base64: string) => { 40 | const data = toByteArray(base64); 41 | const id = bytesToNumber(data.slice(0, 4)); 42 | const resolver = requests.get(id); 43 | resolver(data.slice(4)); 44 | requests.delete(id); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /lib/bridge/platform/node.ts: -------------------------------------------------------------------------------- 1 | import { Bridge } from ".."; 2 | import { deserializeArgs } from "../serialization"; 3 | 4 | export const BridgeNode: Bridge = async ( 5 | payload: Uint8Array, 6 | transformer?: (responseArgs: any[]) => any 7 | ) => { 8 | const response = await fetch("/call", { 9 | method: "POST", 10 | body: payload 11 | }); 12 | const data = new Uint8Array(await response.arrayBuffer()); 13 | const args = deserializeArgs(data); 14 | 15 | if (transformer) { 16 | return transformer(args); 17 | } 18 | 19 | return args; 20 | }; 21 | 22 | export function initCallbackNode() { 23 | const url = new URL(globalThis.location.href); 24 | url.protocol = "ws:"; 25 | const ws = new WebSocket(url.toString()); 26 | ws.onmessage = (e) => { 27 | const [type, message] = JSON.parse(e.data); 28 | globalThis.oncoremessage(type, message); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /lib/bridge/platform/wasm.ts: -------------------------------------------------------------------------------- 1 | import { Bridge } from ".."; 2 | import { toByteArray } from "../../base64"; 3 | import { deserializeArgs } from "../serialization"; 4 | 5 | export const BridgeWasm: Bridge = async ( 6 | payload: Uint8Array, 7 | transformer?: (responseArgs: any[]) => any 8 | ) => { 9 | const response = await globalThis.lib.call(payload); 10 | const args = deserializeArgs(toByteArray(response)); 11 | 12 | if (transformer) { 13 | return transformer(args); 14 | } 15 | 16 | return args; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/bridge/platform/windows.ts: -------------------------------------------------------------------------------- 1 | import { Bridge } from ".."; 2 | import { fromByteArray, toByteArray } from "../../base64"; 3 | import { 4 | bytesToNumber, 5 | deserializeArgs, 6 | getLowestKeyIdAvailable, 7 | numberTo4Bytes 8 | } from "../serialization"; 9 | 10 | const requests = new Map void>(); 11 | 12 | export const BridgeWindows: Bridge = ( 13 | payload: Uint8Array, 14 | transformer?: (responseArgs: any[]) => any 15 | ) => { 16 | const requestId = getLowestKeyIdAvailable(requests); 17 | 18 | requests.set(requestId, null); 19 | 20 | const base64 = fromByteArray( 21 | new Uint8Array([...numberTo4Bytes(requestId), ...payload]) 22 | ); 23 | 24 | return new Promise((resolve, reject) => { 25 | requests.set(requestId, (data) => { 26 | try { 27 | const args = deserializeArgs(data); 28 | if (transformer) { 29 | return resolve(transformer(args)); 30 | } 31 | resolve(args); 32 | } catch (e) { 33 | reject(e); 34 | } 35 | }); 36 | 37 | globalThis.chrome.webview.postMessage(base64); 38 | }); 39 | }; 40 | 41 | export function initRespondWindows() { 42 | globalThis.respond = (base64: string) => { 43 | const data = toByteArray(base64); 44 | const id = bytesToNumber(data.slice(0, 4)); 45 | const resolver = requests.get(id); 46 | resolver(data.slice(4)); 47 | requests.delete(id); 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /lib/components/snackbar.scss: -------------------------------------------------------------------------------- 1 | @use "../../node_modules/@fullstacked/ui/values/spacing.scss"; 2 | @use "../../node_modules/@fullstacked/ui/values/colors.scss"; 3 | @use "../../node_modules/@fullstacked/ui/values/typography.scss"; 4 | 5 | .snack-bars-container { 6 | display: flex; 7 | flex-direction: column; 8 | gap: spacing.$small; 9 | position: fixed; 10 | bottom: 0; 11 | left: 0; 12 | z-index: 100; 13 | padding: 0 spacing.$medium spacing.$medium; 14 | align-items: flex-start; 15 | width: 100%; 16 | pointer-events: none; 17 | text-align: left; 18 | font-family: typography.$fonts; 19 | font-size: typography.$medium; 20 | 21 | max-width: 450px; 22 | @media (max-width: 450px) { 23 | align-items: center; 24 | } 25 | 26 | .snack-bar { 27 | pointer-events: all; 28 | 29 | background-color: colors.$gray-dark; 30 | border-radius: spacing.$x-small; 31 | color: colors.$light; 32 | 33 | min-height: 42px; 34 | max-width: 100%; 35 | width: max-content; 36 | 37 | display: flex; 38 | align-items: center; 39 | justify-content: space-between; 40 | gap: spacing.$small; 41 | 42 | padding: spacing.$x-small spacing.$small; 43 | 44 | box-shadow: 0px 4px 10px colors.opacity(colors.$dark, 60); 45 | 46 | > div:first-child { 47 | padding: spacing.$x-small 0; 48 | width: 100%; 49 | overflow-wrap: break-word; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/components/snackbar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file must follow the figma design 3 | https://www.figma.com/design/xb3JBRCvEWpbwGda03T5QQ/Mockups?node-id=415-3655 4 | */ 5 | 6 | import type { Button } from "@fullstacked/ui"; 7 | 8 | type SnackBarOpt = { 9 | message: string; 10 | autoDismissTimeout?: number; 11 | button?: ReturnType; 12 | }; 13 | 14 | let snackBarsContainer: HTMLDivElement; 15 | 16 | export function SnackBar(opts: SnackBarOpt) { 17 | if (!snackBarsContainer) { 18 | snackBarsContainer = document.createElement("div"); 19 | snackBarsContainer.classList.add("snack-bars-container"); 20 | document.body.append(snackBarsContainer); 21 | } 22 | 23 | const container = document.createElement("div"); 24 | container.classList.add("snack-bar"); 25 | 26 | const text = document.createElement("div"); 27 | text.innerHTML = opts.message; 28 | container.append(text); 29 | 30 | if (opts.button) { 31 | container.append(opts.button); 32 | } 33 | 34 | container.style.transform = "translateY(100%)"; 35 | container.style.transition = "300ms transform"; 36 | snackBarsContainer.append(container); 37 | setTimeout(() => (container.style.transform = "translateY(0%)")); 38 | 39 | let timeout: ReturnType; 40 | const dismiss = () => { 41 | clearTimeout(timeout); 42 | 43 | const animDuration = 500; 44 | container.style.transition = `${animDuration}ms opacity`; 45 | container.style.opacity = "0"; 46 | setTimeout(() => { 47 | container.remove(); 48 | if (snackBarsContainer?.children.length === 0) { 49 | snackBarsContainer.remove(); 50 | snackBarsContainer = null; 51 | } 52 | }, animDuration); 53 | }; 54 | 55 | if (opts.autoDismissTimeout) { 56 | setTimeout(dismiss, opts.autoDismissTimeout); 57 | } 58 | 59 | return { dismiss }; 60 | } 61 | -------------------------------------------------------------------------------- /lib/core_message/core_message.ts: -------------------------------------------------------------------------------- 1 | import { SnackBar } from "../components/snackbar"; 2 | 3 | const coreMessageListeners = new Map void>>(); 4 | export const addListener = ( 5 | messageType: string, 6 | cb: (message: string) => void 7 | ) => { 8 | let listeners = coreMessageListeners.get(messageType); 9 | if (!listeners) { 10 | listeners = new Set(); 11 | coreMessageListeners.set(messageType, listeners); 12 | } 13 | listeners.add(cb); 14 | 15 | const pending = pendingMessages.get(messageType); 16 | if (pending?.length) { 17 | for (const m of pending) { 18 | cb(m); 19 | } 20 | } 21 | }; 22 | export const removeListener = ( 23 | messageType: string, 24 | cb: (message: string) => void 25 | ) => { 26 | let listeners = coreMessageListeners.get(messageType); 27 | listeners?.delete(cb); 28 | if (listeners?.size === 0) { 29 | coreMessageListeners.delete(messageType); 30 | } 31 | }; 32 | 33 | const pendingMessages = new Map(); 34 | setInterval(() => pendingMessages.clear(), 10 * 1000); // 10s 35 | 36 | globalThis.oncoremessage = (messageType: string, message: string) => { 37 | const listeners = coreMessageListeners.get(messageType); 38 | if (!listeners?.size) { 39 | let pending = pendingMessages.get(messageType); 40 | if (!pending) { 41 | pending = []; 42 | pendingMessages.set(messageType, pending); 43 | } 44 | pending.push(message); 45 | } else { 46 | listeners?.forEach((cb) => cb(message)); 47 | } 48 | }; 49 | 50 | addListener("hello", console.log); 51 | addListener("log", console.log); 52 | addListener("alert", (message) => { 53 | SnackBar({ 54 | message, 55 | autoDismissTimeout: 4000 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/core_message/index.ts: -------------------------------------------------------------------------------- 1 | import * as core_message from "./core_message"; 2 | export default core_message; 3 | export * from "./core_message"; 4 | -------------------------------------------------------------------------------- /lib/fs/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "./fs"; 2 | export default fs; 3 | export * from "./fs"; 4 | -------------------------------------------------------------------------------- /lib/platform/index.ts: -------------------------------------------------------------------------------- 1 | export enum Platform { 2 | NODE = "node", 3 | APPLE = "apple", 4 | ANDROID = "android", 5 | DOCKER = "docker", 6 | WINDOWS = "windows", 7 | WASM = "wasm", 8 | LINUX = "linux" 9 | } 10 | 11 | const platform = await (await fetch("/platform")).text(); 12 | export default platform; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fullstacked/fullstacked", 3 | "version": "0.11.2", 4 | "scripts": { 5 | "build": "esbuild build.ts --bundle --outfile=.cache/build.js --platform=node --format=esm --packages=external && node .cache/build.js", 6 | "start": "npm run build -- --no-zip && npm start -w platform/node", 7 | "fmt": "prettier . --write && cd core && gofmt -l -w .", 8 | "typecheck": "tsc --noEmit", 9 | "test": "esbuild test/index.ts --bundle --outfile=.cache/test.js --platform=node --format=esm --packages=external && node .cache/test.js " 10 | }, 11 | "workspaces": [ 12 | "platform/node", 13 | "platform/wasm" 14 | ], 15 | "author": "FullStacked", 16 | "license": "GPL-3.0", 17 | "type": "module", 18 | "prettier": { 19 | "tabWidth": 4, 20 | "trailingComma": "none" 21 | }, 22 | "dependencies": { 23 | "@fullstacked/code-editor": "github:fullstackedorg/code-editor#3ce05f26fbf14f2ccda5a67ef53d8f45e7017393", 24 | "@fullstacked/file-tree": "github:fullstackedorg/file-tree#8fc6d5e587507c631a68525659745eeda414bcf3", 25 | "@fullstacked/stack-navigation": "github:fullstackedorg/stack-navigation#fd824bc6625401bf49b9030861a3c4150e6e8b82", 26 | "@fullstacked/ui": "github:fullstackedorg/ui#729a7ade46afdd6d58b9d44794705dbd59c1ed30", 27 | "@types/adm-zip": "^0.5.6", 28 | "@types/node": "^22.10.1", 29 | "@types/semver": "^7.5.8", 30 | "adm-zip": "^0.5.16", 31 | "dotenv": "^16.4.5", 32 | "esbuild": "^0.25.3", 33 | "fuse.js": "^7.0.0", 34 | "open": "^10.1.0", 35 | "prettier": "^3.3.3", 36 | "pretty-bytes": "^6.1.1", 37 | "pretty-ms": "^9.1.0", 38 | "puppeteer": "^23.4.1", 39 | "sass": "^1.83.4", 40 | "slugify": "^1.6.6", 41 | "typescript": "^5.8.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /platform/android/.gitignore: -------------------------------------------------------------------------------- 1 | androidpublisher.dat 2 | client_secrets.json -------------------------------------------------------------------------------- /platform/android/publish.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import url from "node:url"; 3 | import fs from "node:fs"; 4 | import child_process from "node:child_process"; 5 | import dotenv from "dotenv"; 6 | import version from "../../version.js"; 7 | 8 | const currentDirectory = path.dirname(url.fileURLToPath(import.meta.url)); 9 | const rootDirectory = path.resolve(currentDirectory, "..", ".."); 10 | 11 | // build editor 12 | 13 | child_process.execSync("npm run build -- --production", { 14 | cwd: rootDirectory, 15 | stdio: "inherit" 16 | }); 17 | 18 | // build core 19 | 20 | child_process.execSync("make android -j4", { 21 | cwd: path.resolve(rootDirectory, "core", "build"), 22 | stdio: "inherit" 23 | }); 24 | 25 | // update version 26 | 27 | const studioDirectory = path.resolve(currentDirectory, "studio"); 28 | const gradleFile = path.resolve(studioDirectory, "app", "build.gradle.kts"); 29 | const gradleFileContent = fs.readFileSync(gradleFile, { 30 | encoding: "utf-8" 31 | }); 32 | const gradleFileUpdated = gradleFileContent 33 | .replace( 34 | /versionName = ".*?"/g, 35 | `versionName = "${version.major}.${version.minor}.${version.patch}"` 36 | ) 37 | .replace(/versionCode = .*?\n/g, `versionCode = ${version.build}\n`); 38 | fs.writeFileSync(gradleFile, gradleFileUpdated); 39 | 40 | const androidKeys = dotenv.parse( 41 | fs.readFileSync(path.resolve(currentDirectory, "ANDROID_KEYS.env")) 42 | ); 43 | 44 | // gradle build 45 | 46 | child_process.execSync("./gradlew bundleRelease", { 47 | cwd: studioDirectory, 48 | stdio: "inherit", 49 | env: { 50 | JAVA_HOME: androidKeys.JAVA_HOME 51 | } 52 | }); 53 | 54 | // pkg sign 55 | 56 | const bundle = path.resolve( 57 | studioDirectory, 58 | "app", 59 | "build", 60 | "outputs", 61 | "bundle", 62 | "release", 63 | "app-release.aab" 64 | ); 65 | 66 | child_process.execSync( 67 | `jarsigner -keystore ${androidKeys.FILE} -storepass ${androidKeys.PASSPHRASE} ${bundle} ${androidKeys.KEY}`, 68 | { 69 | cwd: studioDirectory, 70 | stdio: "inherit" 71 | } 72 | ); 73 | 74 | // play console upload 75 | 76 | child_process.execSync( 77 | `python upload.py org.fullstacked.editor ${bundle} ${version.major}.${version.minor}.${version.patch}`, 78 | { 79 | stdio: "inherit", 80 | cwd: currentDirectory 81 | } 82 | ); 83 | -------------------------------------------------------------------------------- /platform/android/studio/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /platform/android/studio/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/main/cpp/core 3 | /release -------------------------------------------------------------------------------- /platform/android/studio/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version "8.7.3" 3 | id("org.jetbrains.kotlin.android") version "1.9.25" 4 | } 5 | 6 | android { 7 | namespace = "org.fullstacked.editor" 8 | compileSdk = 34 9 | 10 | ndkVersion = "26.1.10909125" 11 | 12 | defaultConfig { 13 | applicationId = "org.fullstacked.editor" 14 | minSdk = 29 15 | targetSdk = 34 16 | versionCode = 1020 17 | versionName = "0.11.2" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | ndk { 24 | abiFilters += listOf("armeabi-v7a","arm64-v8a", "x86_64") 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_20 39 | targetCompatibility = JavaVersion.VERSION_20 40 | } 41 | kotlinOptions { 42 | jvmTarget = "20" 43 | } 44 | buildFeatures { 45 | compose = true 46 | } 47 | composeOptions { 48 | kotlinCompilerExtensionVersion = "1.5.15" 49 | } 50 | sourceSets { 51 | getByName("main") { 52 | assets { 53 | srcDirs("../../../../out/zip") 54 | } 55 | } 56 | } 57 | externalNativeBuild { 58 | cmake { 59 | path = file("src/main/cpp/CMakeLists.txt") 60 | version = "3.22.1" 61 | } 62 | } 63 | } 64 | //noinspection UseTomlInstead 65 | dependencies { 66 | implementation(libs.androidx.core.ktx) 67 | implementation(libs.androidx.lifecycle.runtime.ktx) 68 | implementation(libs.androidx.activity.compose) 69 | implementation(platform(libs.androidx.compose.bom)) 70 | implementation(libs.androidx.ui) 71 | implementation(libs.androidx.ui.graphics) 72 | implementation(libs.androidx.ui.tooling.preview) 73 | implementation(libs.androidx.material3) 74 | testImplementation(libs.junit) 75 | androidTestImplementation(libs.androidx.junit) 76 | androidTestImplementation(libs.androidx.espresso.core) 77 | androidTestImplementation(platform(libs.androidx.compose.bom)) 78 | androidTestImplementation(libs.androidx.ui.test.junit4) 79 | debugImplementation(libs.androidx.ui.tooling) 80 | debugImplementation(libs.androidx.ui.test.manifest) 81 | } -------------------------------------------------------------------------------- /platform/android/studio/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html. 4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples. 5 | 6 | # Sets the minimum CMake version required for this project. 7 | cmake_minimum_required(VERSION 3.22.1) 8 | 9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, 10 | # Since this is the top level CMakeLists.txt, the project name is also accessible 11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level 12 | # build script scope). 13 | project(editor-core) 14 | 15 | set(core_DIR ${CMAKE_CURRENT_SOURCE_DIR}/core) 16 | 17 | if(${ANDROID_ABI} MATCHES "arm64-v8a") 18 | add_compile_definitions(ANDROID_ABI_arm64) 19 | endif () 20 | 21 | if(${ANDROID_ABI} MATCHES "x86_64") 22 | add_compile_definitions(ANDROID_ABI_x64) 23 | endif () 24 | 25 | add_library(core SHARED IMPORTED) 26 | set_target_properties(core PROPERTIES IMPORTED_LOCATION 27 | ${core_DIR}/${ANDROID_ABI}/core.so) 28 | 29 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 30 | 31 | # Creates and names a library, sets it as either STATIC 32 | # or SHARED, and provides the relative paths to its source code. 33 | # You can define multiple libraries, and CMake builds them for you. 34 | # Gradle automatically packages shared libraries with your APK. 35 | # 36 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define 37 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} 38 | # is preferred for the same purpose. 39 | # 40 | # In order to load a library into your app from Java/Kotlin, you must call 41 | # System.loadLibrary() and pass the name of the library defined here; 42 | # for GameActivity/NativeActivity derived applications, the same library name must be 43 | # used in the AndroidManifest.xml file. 44 | add_library(editor-core SHARED 45 | # List C/C++ source files with relative paths to this CMakeLists.txt. 46 | bridge.cpp) 47 | 48 | # Specifies libraries CMake should link to your target library. You 49 | # can link libraries from various origins, such as libraries defined in this 50 | # build script, prebuilt third-party libraries, or Android system libraries. 51 | target_link_libraries(editor-core 52 | android 53 | core 54 | log) 55 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/cpp/bridge.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Charles-Philippe Lepage on 2024-07-25. 3 | // 4 | 5 | #ifdef ANDROID_ABI_arm64 6 | #include "core/arm64-v8a/core.h" 7 | #elif ANDROID_ABI_x64 8 | #include "core/x86_64/core.h" 9 | #else 10 | #include "core/armeabi-v7a/core.h" 11 | #endif 12 | 13 | #include 14 | 15 | #ifndef FULLSTACKED_EDITOR_EDITOR_H 16 | #define FULLSTACKED_EDITOR_EDITOR_H 17 | 18 | extern "C" { 19 | JNIEXPORT void JNICALL Java_org_fullstacked_editor_MainActivity_directories 20 | (JNIEnv *env, jobject jobj, jstring root, jstring config, jstring editor); 21 | 22 | JNIEXPORT jbyteArray JNICALL Java_org_fullstacked_editor_Instance_call 23 | (JNIEnv *env, jobject jobj, jbyteArray buffer); 24 | 25 | JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved); 26 | 27 | JNIEXPORT void JNICALL Java_org_fullstacked_editor_MainActivity_addCallback(JNIEnv *env, jobject thiz, jint id); 28 | JNIEXPORT void JNICALL Java_org_fullstacked_editor_MainActivity_removeCallback(JNIEnv *env, jobject thiz, jint id); 29 | 30 | } 31 | 32 | #endif //FULLSTACKED_EDITOR_EDITOR_H 33 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/java/org/fullstacked/editor/Instance.kt: -------------------------------------------------------------------------------- 1 | package org.fullstacked.editor 2 | 3 | class Instance(val projectId: String, val isEditor: Boolean = false) { 4 | private var headerRequest: ByteArray 5 | 6 | private external fun call(buffer: ByteArray): ByteArray 7 | 8 | init { 9 | if(this.isEditor) { 10 | this.headerRequest = byteArrayOf( 11 | 1 // isEditor 12 | ) 13 | this.headerRequest += numberToBytes(0) // no project id 14 | } else { 15 | this.headerRequest = byteArrayOf( 16 | 0 // is not Editor 17 | ) 18 | val idData = this.projectId.toByteArray() 19 | this.headerRequest += numberToBytes(idData.size) 20 | this.headerRequest += idData 21 | } 22 | } 23 | 24 | fun callLib(payload: ByteArray) : ByteArray { 25 | return this.call(this.headerRequest + payload) 26 | } 27 | } -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/drawable/fullstacked_app_icon_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #007AFF 4 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FullStacked 3 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /platform/android/studio/app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /platform/android/studio/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.7.3" apply false 4 | id("org.jetbrains.kotlin.android") version "1.9.25" apply false 5 | } -------------------------------------------------------------------------------- /platform/android/studio/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /platform/android/studio/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.7.1" 3 | kotlin = "1.9.0" 4 | coreKtx = "1.10.1" 5 | junit = "4.13.2" 6 | junitVersion = "1.1.5" 7 | espressoCore = "3.5.1" 8 | lifecycleRuntimeKtx = "2.6.1" 9 | activityCompose = "1.8.0" 10 | composeBom = "2024.04.01" 11 | 12 | [libraries] 13 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 14 | junit = { group = "junit", name = "junit", version.ref = "junit" } 15 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 16 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 17 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 18 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 19 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 20 | androidx-ui = { group = "androidx.compose.ui", name = "ui" } 21 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 22 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 23 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 24 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 25 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } 26 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 27 | 28 | [plugins] 29 | android-application = { id = "com.android.application", version.ref = "agp" } 30 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 31 | 32 | -------------------------------------------------------------------------------- /platform/android/studio/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/android/studio/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /platform/android/studio/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 03 11:55:39 EDT 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /platform/android/studio/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /platform/android/studio/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "FullStacked Editor" 23 | include(":app") 24 | -------------------------------------------------------------------------------- /platform/android/upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2014 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the 'License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Uploads an Android App Bundle to the internal test track.""" 18 | 19 | import argparse 20 | import sys 21 | from apiclient import sample_tools 22 | from oauth2client import client 23 | import mimetypes 24 | mimetypes.add_type('application/octet-stream', '.aab') 25 | 26 | TRACK = 'internal' # Can be 'internal', 'alpha', beta', 'production' or 'rollout' 27 | 28 | # Declare command-line flags. 29 | argparser = argparse.ArgumentParser(add_help=False) 30 | argparser.add_argument('package_name', 31 | help='The package name. Example: com.android.sample') 32 | argparser.add_argument('aab_file', 33 | help='The path to the .aab file to upload.') 34 | argparser.add_argument('app_version', 35 | help='x.x.x') 36 | 37 | 38 | def main(argv): 39 | # Authenticate and construct service. 40 | service, flags = sample_tools.init( 41 | argv, 42 | 'androidpublisher', 43 | 'v3', 44 | __doc__, 45 | __file__, parents=[argparser], 46 | scope='https://www.googleapis.com/auth/androidpublisher') 47 | 48 | # Process flags and read their values. 49 | package_name = flags.package_name 50 | aab_file = flags.aab_file 51 | app_version = flags.app_version 52 | 53 | try: 54 | edit_request = service.edits().insert(body={}, packageName=package_name) 55 | result = edit_request.execute() 56 | edit_id = result['id'] 57 | 58 | aab_response = service.edits().bundles().upload( 59 | editId=edit_id, 60 | packageName=package_name, 61 | media_body=aab_file).execute() 62 | 63 | print 'Version code %d has been uploaded' % aab_response['versionCode'] 64 | 65 | track_response = service.edits().tracks().update( 66 | editId=edit_id, 67 | track=TRACK, 68 | packageName=package_name, 69 | body={u'releases': [{ 70 | u'name': str(app_version), 71 | u'versionCodes': [str(aab_response['versionCode'])], 72 | u'status': u'completed', 73 | }]}).execute() 74 | 75 | print 'Track %s is set with releases: %s' % ( 76 | track_response['track'], str(track_response['releases'])) 77 | 78 | commit_request = service.edits().commit( 79 | editId=edit_id, packageName=package_name).execute() 80 | 81 | print 'Edit "%s" has been committed' % (commit_request['id']) 82 | 83 | except client.AccessTokenRefreshError: 84 | print ('The credentials have been revoked or expired, please re-run the ' 85 | 'application to re-authorize') 86 | 87 | if __name__ == '__main__': 88 | main(sys.argv) -------------------------------------------------------------------------------- /platform/apple/.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | xcshareddata 3 | *.xcarchive 4 | pkg* 5 | exportOptions.plist -------------------------------------------------------------------------------- /platform/apple/FullStacked.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/100-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/100-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/1024-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/1024-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/114-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/114-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/120-bleed 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/120-bleed 1.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/120-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/120-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/144-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/144-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/152-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/152-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/167-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/167-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/180-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/180-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/20-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/20-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/29-bleed 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/29-bleed 1.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/29-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/29-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/40-bleed 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/40-bleed 1.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/40-bleed 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/40-bleed 2.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/40-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/40-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/50-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/50-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/57-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/57-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/58-bleed 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/58-bleed 1.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/58-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/58-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/60-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/60-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/72-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/72-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/76-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/76-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/80-bleed 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/80-bleed 1.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/80-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/80-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/87-bleed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/apple/FullStacked/Assets.xcassets/AppIcon.appiconset/87-bleed.png -------------------------------------------------------------------------------- /platform/apple/FullStacked/FullStacked.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/FullStacked.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct FullStackedApp: App { 5 | static var singleton: FullStackedApp? 6 | @ObservedObject var webViews = WebViews() 7 | 8 | init() { 9 | FullStackedApp.singleton = self; 10 | 11 | setDirectories() 12 | setCallback() 13 | } 14 | 15 | var body: some Scene { 16 | #if os(macOS) 17 | Window("FullStacked", id: "Editor"){ 18 | WebViewsStacked(webViews: self.webViews) 19 | .onDisappear { 20 | exit(0) 21 | } 22 | } 23 | #else 24 | WindowGroup("FullStacked"){ 25 | if #available(iOS 16.0, *) { 26 | WebViewsStacked(webViews: self.webViews) 27 | .onDisappear { 28 | exit(0) 29 | } 30 | } else { 31 | WebViewsStackedLegacy(webViews: self.webViews) 32 | .onDisappear{ 33 | exit(0) 34 | } 35 | } 36 | } 37 | #endif 38 | 39 | if #available(iOS 16.1, *) { 40 | WindowGroup(id: "window-webview", for: String.self) { $projectId in 41 | WebViewSingle(projectId: projectId) 42 | } 43 | .commands { 44 | CommandGroup(replacing: CommandGroupPlacement.newItem) { 45 | EmptyView() 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLName 11 | editor.FullStacked 12 | CFBundleURLSchemes 13 | 14 | fullstacked 15 | 16 | 17 | 18 | ITSAppUsesNonExemptEncryption 19 | 20 | NSBonjourServices 21 | 22 | _fullstacked._tcp 23 | _fullstacked-ios._tcp 24 | 25 | UIFileSharingEnabled 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/Instance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Instance.swift 3 | // FullStacked 4 | // 5 | // Created by Charles-Philippe Lepage on 2024-11-06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class Instance { 11 | public let isEditor: Bool 12 | public let id: String 13 | private var header: Data 14 | 15 | 16 | init (projectId: String, isEditor: Bool = false) { 17 | self.isEditor = isEditor 18 | self.id = projectId 19 | 20 | self.header = Data() 21 | if(isEditor) { 22 | self.header.append(Data([1])) // isEditor 23 | self.header.append(0.toBytes()) // no project id 24 | } else { 25 | self.header.append(Data([0])) 26 | let projectIdData = self.id.data(using: .utf8)! 27 | self.header.append(projectIdData.count.toBytes()) 28 | self.header.append(projectIdData) 29 | } 30 | } 31 | 32 | func callLib(payload: Data) -> Data { 33 | var data = Data() 34 | data.append(self.header) 35 | data.append(payload) 36 | 37 | var responsePtr = Data().ptr() 38 | let size = call(data.ptr(), Int32(data.count), &responsePtr) 39 | let responseDataPtr = UnsafeBufferPointer(start: responsePtr!.assumingMemoryBound(to: UInt8.self), count: Int(size)) 40 | let responseData = Data(responseDataPtr) 41 | freePtr(responsePtr) 42 | 43 | return responseData 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategorySystemBootTime 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | 35F9.1 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/WebViewAppKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewRepresentable.swift 3 | // FullStacked 4 | // 5 | // Created by Charles-Philippe Lepage on 2024-11-27. 6 | // 7 | import SwiftUI 8 | @preconcurrency import WebKit 9 | 10 | // MacOS 11 | 12 | class WebViewExtended: WKWebView, WKUIDelegate { 13 | override init(frame: CGRect, configuration: WKWebViewConfiguration){ 14 | super.init(frame: frame, configuration: configuration) 15 | self.uiDelegate = self 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | func openBrowserURL(_ url: URL){ 23 | NSWorkspace.shared.open(url) 24 | } 25 | 26 | func openDownloadDirectory(){ 27 | NSWorkspace.shared.open(URL(fileURLWithPath: downloadDirectory)) 28 | } 29 | 30 | func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { 31 | let openPanel = NSOpenPanel() 32 | openPanel.canChooseFiles = true 33 | openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection 34 | openPanel.begin { (result) in 35 | if result == NSApplication.ModalResponse.OK { 36 | completionHandler(openPanel.urls) 37 | } else if result == NSApplication.ModalResponse.cancel { 38 | completionHandler(nil) 39 | } 40 | } 41 | } 42 | } 43 | 44 | // suppress "funk" noise 45 | // source: https://stackoverflow.com/a/69858444 46 | class KeyView: NSView { 47 | override var acceptsFirstResponder: Bool { true } 48 | override func keyDown(with event: NSEvent) {} 49 | } 50 | 51 | struct WebViewRepresentable: NSViewRepresentable { 52 | private let webview: WebView; 53 | init(webView: WebView) { 54 | self.webview = webView 55 | } 56 | 57 | func makeNSView(context: Context) -> NSView { 58 | let view = KeyView() 59 | DispatchQueue.main.async { 60 | view.window?.makeFirstResponder(view) 61 | } 62 | self.webview.autoresizingMask = [.width, .height] 63 | view.addSubview(self.webview); 64 | return view 65 | } 66 | 67 | 68 | func updateNSView(_ uiView: NSView, context: Context) { } 69 | } 70 | -------------------------------------------------------------------------------- /platform/apple/FullStacked/WebViewUIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewRepresentable.swift 3 | // FullStacked 4 | // 5 | // Created by Charles-Philippe Lepage on 2024-11-27. 6 | // 7 | import SwiftUI 8 | import WebKit 9 | 10 | // iOS 11 | 12 | class WebViewExtended: WKWebView { 13 | override var safeAreaInsets: UIEdgeInsets { 14 | return UIEdgeInsets(top: super.safeAreaInsets.top, left: 0, bottom: 0, right: 0) 15 | } 16 | 17 | func openBrowserURL(_ url: URL){ 18 | if( UIApplication.shared.canOpenURL(url)) { 19 | UIApplication.shared.open(url) 20 | } 21 | } 22 | 23 | func openDownloadDirectory(){ 24 | UIApplication.shared.open(URL(string: "shareddocuments://" + downloadDirectory)!) 25 | } 26 | } 27 | 28 | struct WebViewRepresentable: UIViewRepresentable { 29 | private let webView: WebView; 30 | init(webView: WebView) { 31 | self.webView = webView 32 | } 33 | 34 | func makeUIView(context: Context) -> WebView { 35 | return webView 36 | } 37 | 38 | func updateUIView(_ uiView: WebView, context: Context) { 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /platform/linux/.gitignore: -------------------------------------------------------------------------------- 1 | fullstacked.deb -------------------------------------------------------------------------------- /platform/linux/app.cpp: -------------------------------------------------------------------------------- 1 | #include "./app.h" 2 | #include 3 | 4 | App::App() 5 | { 6 | App::instance = this; 7 | app = Gtk::Application::create("fullstacked"); 8 | } 9 | 10 | void App::onMessage(char *projectId, char *type, char *message) 11 | { 12 | auto exists = windows.find(projectId); 13 | if (exists != windows.end()) 14 | { 15 | exists->second->onMessage(type, message); 16 | } 17 | } 18 | 19 | void App::onClose(GtkWidget *widget, gpointer user_data) 20 | { 21 | auto i = static_cast(user_data); 22 | App::instance->windows.erase(i->id); 23 | delete i; 24 | }; 25 | 26 | void App::open(std::string projectId, bool isEditor) 27 | { 28 | auto exists = windows.find(projectId); 29 | if (exists != windows.end()) 30 | { 31 | exists->second->show(); 32 | exists->second->present(); 33 | exists->second->fullscreen(); 34 | webkit_web_view_reload(exists->second->webview); 35 | } 36 | else 37 | { 38 | auto win = new Instance(projectId, isEditor); 39 | windows[projectId] = win; 40 | if(kiosk) { 41 | win->signal_realize().connect([&]{ 42 | win->fullscreen(); 43 | }); 44 | } 45 | win->show(); 46 | app->add_window(*win); 47 | } 48 | } 49 | 50 | int App::run(std::string startupId) 51 | { 52 | app->signal_startup().connect([&]{ open(startupId, startupId == ""); }); 53 | return app->run(); 54 | } -------------------------------------------------------------------------------- /platform/linux/app.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_H 2 | #define APP_H 3 | 4 | #include 5 | #include "./instance.h" 6 | 7 | class App 8 | { 9 | private: 10 | Glib::RefPtr app; 11 | 12 | public: 13 | inline static App *instance; 14 | std::map windows; 15 | std::string deeplink; 16 | bool kiosk = false; 17 | 18 | App(); 19 | 20 | void onMessage(char *projectId, char* type, char* message); 21 | 22 | void open(std::string projectId, bool isEditor); 23 | 24 | static void onClose(GtkWidget* widget, gpointer user_data); 25 | 26 | int run(std::string startupId); 27 | }; 28 | 29 | #endif -------------------------------------------------------------------------------- /platform/linux/build.sh: -------------------------------------------------------------------------------- 1 | # ./build.sh [arm64|x86_64] 2 | cp bin/linux-$1.h bin/linux.h 3 | 4 | rm -rf out 5 | 6 | mkdir -p ./out/usr/share/fullstacked 7 | cp -r ../../out/editor ./out/usr/share/fullstacked 8 | 9 | mkdir ./out/DEBIAN 10 | cp control out/DEBIAN/control 11 | 12 | mkdir -p out/usr/bin 13 | cp -r ../../core/bin . 14 | g++ utils.cpp instance.cpp app.cpp main.cpp bin/linux-$1 -o out/usr/bin/fullstacked `pkg-config gtkmm-4.0 webkitgtk-6.0 --libs --cflags` -------------------------------------------------------------------------------- /platform/linux/control: -------------------------------------------------------------------------------- 1 | Package: fullstacked 2 | Version: 0.1 3 | Maintainer: cplepage 4 | Architecture: amd64 5 | Description: FullStacked 6 | -------------------------------------------------------------------------------- /platform/linux/fix.sh: -------------------------------------------------------------------------------- 1 | # update broken apparmor - bubblewrap 2 | sudo add-apt-repository ppa:apparmor-dev/apparmor-sru 3 | sudo apt update 4 | sudo apt install apparmor -------------------------------------------------------------------------------- /platform/linux/instance.h: -------------------------------------------------------------------------------- 1 | #ifndef INSTANCE_H_ 2 | #define INSTANCE_H_ 3 | 4 | #include 5 | #include 6 | 7 | class Instance : public Gtk::Window 8 | { 9 | private: 10 | bool isEditor; 11 | char *header; 12 | int headerSize; 13 | bool firstTouch; 14 | WebKitUserContentManager *ucm; 15 | 16 | public: 17 | std::string id; 18 | WebKitWebView *webview; 19 | 20 | static void webKitURISchemeRequestCallback(WebKitURISchemeRequest *request, gpointer userData); 21 | 22 | static void onScriptMessage(WebKitUserContentManager *manager, JSCValue *value, gpointer userData); 23 | 24 | static gboolean navigationDecidePolicy(WebKitWebView *view, 25 | WebKitPolicyDecision *decision, 26 | WebKitPolicyDecisionType decision_type, 27 | gpointer user_data); 28 | 29 | bool on_window_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state); 30 | 31 | Instance(std::string pId, bool pIsEditor); 32 | ~Instance(); 33 | 34 | std::vector callLib(char *data, int size); 35 | 36 | void onMessage(char* type, char* message); 37 | }; 38 | 39 | #endif -------------------------------------------------------------------------------- /platform/linux/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "./app.h" 6 | #include "./bin/linux.h" 7 | #include 8 | #include 9 | 10 | std::string getExePath() 11 | { 12 | char result[PATH_MAX]; 13 | ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); 14 | std::string path = std::string(result, (count > 0) ? count : 0); 15 | return path; 16 | } 17 | 18 | std::string getEditorDir() 19 | { 20 | std::string path = getExePath(); 21 | int pos = path.find_last_of("/"); 22 | std::string dir = path.substr(0, pos); 23 | pos = dir.find_last_of("/"); 24 | dir = path.substr(0, pos); 25 | return dir + "/share/fullstacked/editor"; 26 | } 27 | 28 | void setDirectories() 29 | { 30 | std::string home = getenv("HOME"); 31 | std::string root = home + "/FullStacked"; 32 | std::string config = home + "/.config/fullstacked"; 33 | std::string editor = getEditorDir(); 34 | 35 | directories( 36 | root.data(), 37 | config.data(), 38 | editor.data()); 39 | } 40 | 41 | void libCallback(char *projectId, char *type, char *msg) 42 | { 43 | App::instance->onMessage(projectId, type, msg); 44 | } 45 | 46 | void registerDesktopApp() 47 | { 48 | std::string localIconsDir = std::string(getenv("HOME")) + "/.local/share/icons"; 49 | std::filesystem::create_directories(localIconsDir); 50 | std::string appIconFile = getEditorDir() + "/assets/dev-icon.png"; 51 | std::filesystem::copy_file(appIconFile, localIconsDir + "/fullstacked.png", std::filesystem::copy_options::overwrite_existing); 52 | 53 | std::string localAppsDir = std::string(getenv("HOME")) + "/.local/share/applications"; 54 | std::filesystem::create_directories(localAppsDir); 55 | std::ofstream localAppFile(localAppsDir + "/fullstacked.desktop"); 56 | std::string contents = 57 | "[Desktop Entry]\n" 58 | "Name=FullStacked\n" 59 | "Exec=" + 60 | getExePath() + " %u\n" 61 | "Terminal=false\n" 62 | "Type=Application\n" 63 | "MimeType=x-scheme-handler/fullstacked\n" 64 | "Icon=fullstacked\n" 65 | "Categories=Development;Utility;"; 66 | localAppFile << contents.c_str(); 67 | localAppFile.close(); 68 | 69 | system(("update-desktop-database " + localAppsDir).c_str()); 70 | } 71 | 72 | int main(int argc, char *argv[]) 73 | { 74 | registerDesktopApp(); 75 | setDirectories(); 76 | callback((void *)libCallback); 77 | auto app = new App(); 78 | 79 | std::string httpPrefix = "http"; 80 | std::string kioskFlag = "--kiosk"; 81 | std::string startupId = ""; 82 | for(int i = 1; i < argc; i++) { 83 | std::string arg(argv[i]); 84 | 85 | if(arg.compare(0, httpPrefix.size(), httpPrefix) == 0) { 86 | app->deeplink = arg; 87 | } else if(arg == kioskFlag) { 88 | app->kiosk = true; 89 | if(argc > i + 1) { 90 | startupId = std::string(argv[i + 1]); 91 | i++; 92 | } 93 | } 94 | } 95 | 96 | return app->run(startupId); 97 | } -------------------------------------------------------------------------------- /platform/linux/pkg.sh: -------------------------------------------------------------------------------- 1 | dpkg-deb --build ./out 2 | mv out.deb fullstacked.deb -------------------------------------------------------------------------------- /platform/linux/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | 7 | void numberToCharPtr(int number, char *ptr); 8 | 9 | unsigned bytesToNumber(unsigned char *bytes, int size); 10 | 11 | int deserializeNumber(char *bytes, int size); 12 | 13 | void printBuffer(char *buffer, int size); 14 | 15 | int combineBuffers(char *buf1, int lgt1, char *buf2, int lgt2, char *result); 16 | 17 | class DataValue 18 | { 19 | public: 20 | bool boolean; 21 | std::string str; 22 | int number; 23 | std::vector buffer; 24 | }; 25 | 26 | enum DataType 27 | { 28 | UNDEFINED = 0, 29 | BOOLEAN = 1, 30 | STRING = 2, 31 | NUMBER = 3, 32 | BUFFER = 4 33 | }; 34 | 35 | std::vector deserializeArgs(std::vector data); 36 | 37 | std::string gen_random(const int len); 38 | 39 | std::string uri_decode(std::string str); 40 | 41 | struct URL 42 | { 43 | public: 44 | URL(const std::string &url_s) 45 | { 46 | this->parse(url_s); 47 | } 48 | std::string protocol, host, path, query; 49 | 50 | std::string str(); 51 | 52 | private: 53 | void parse(const std::string &url_s); 54 | }; 55 | 56 | #endif -------------------------------------------------------------------------------- /platform/node/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | *.tgz 3 | js 4 | editor 5 | index.js 6 | Demo.zip -------------------------------------------------------------------------------- /platform/node/.npmignore: -------------------------------------------------------------------------------- 1 | .cache 2 | src 3 | *.ts 4 | !*.d.ts 5 | *.tgz 6 | !dist 7 | !Demo.zip -------------------------------------------------------------------------------- /platform/node/build.ts: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | 3 | esbuild.buildSync({ 4 | entryPoints: ["src/index.ts"], 5 | outfile: "index.js", 6 | bundle: true, 7 | format: "esm", 8 | packages: "external", 9 | platform: "node" 10 | }); 11 | -------------------------------------------------------------------------------- /platform/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "esbuild build.ts --outfile=.cache/build.js --packages=external && node .cache/build.js", 4 | "start": "npm run build && node index.js", 5 | "prepack": "npm run build" 6 | }, 7 | "type": "module", 8 | "dependencies": { 9 | "@types/ws": "^8.5.13", 10 | "fast-querystring": "^1.1.2", 11 | "ffi-rs": "^1.0.98", 12 | "ws": "^8.18.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /platform/node/src/build.ts: -------------------------------------------------------------------------------- 1 | import type esbuild from "esbuild"; 2 | 3 | export function build( 4 | buildSync: typeof esbuild.buildSync, 5 | input: string, 6 | out: string, 7 | outdir: string, 8 | nodePath: string, 9 | sourcemap: esbuild.BuildOptions["sourcemap"] = "inline", 10 | splitting = true, 11 | minify: esbuild.BuildOptions["minify"] = false 12 | ) { 13 | try { 14 | buildSync({ 15 | entryPoints: [ 16 | { 17 | in: input, 18 | out 19 | } 20 | ], 21 | outdir, 22 | splitting, 23 | bundle: true, 24 | format: "esm", 25 | sourcemap, 26 | write: true, 27 | nodePaths: nodePath ? [nodePath] : undefined, 28 | logLevel: "silent", 29 | minify 30 | }); 31 | } catch (e) { 32 | return { errors: e.errors as esbuild.ResolveResult["errors"] }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /platform/node/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import os from "os"; 3 | import { setCallback, setDirectories } from "./call"; 4 | import { createWebView } from "./webview"; 5 | import { createInstance } from "./instance"; 6 | 7 | let deeplink: string = null, 8 | deeplinkMessaged = false; 9 | if (process.argv.at(-1).startsWith("http")) { 10 | deeplink = process.argv.at(-1); 11 | } 12 | 13 | const root = path.resolve(os.homedir(), "FullStacked"); 14 | await setDirectories({ 15 | root, 16 | config: path.resolve(os.homedir(), ".config", "fullstacked"), 17 | editor: path.resolve(process.cwd(), "..", "..", "out", "editor") 18 | }); 19 | 20 | export const platform = new TextEncoder().encode("node"); 21 | 22 | type WebView = Awaited>; 23 | 24 | const webViews = new Map(); 25 | 26 | const cb = (projectId: string, messageType: string, message: string) => { 27 | if (projectId === "*") { 28 | for (const w of webViews.values()) { 29 | w.message(messageType, message); 30 | } 31 | return; 32 | } else if (!projectId && messageType === "open") { 33 | openProject(message); 34 | return; 35 | } 36 | 37 | const webview = webViews.get(projectId); 38 | webview?.message(messageType, message); 39 | }; 40 | await setCallback(cb); 41 | 42 | async function openProject(id: string) { 43 | let webView = webViews.get(id); 44 | if (webView) { 45 | return; 46 | } 47 | 48 | const instance = createInstance(id); 49 | webView = await createWebView(instance, () => webViews.delete(id)); 50 | webViews.set(id, webView); 51 | } 52 | 53 | const instanceEditor = createInstance("", true); 54 | const instanceWebView = await createWebView(instanceEditor, null, () => { 55 | if (!deeplink || deeplinkMessaged) return; 56 | instanceWebView.message("deeplink", "fullstacked://" + deeplink); 57 | deeplinkMessaged = true; 58 | }); 59 | webViews.set("", instanceWebView); 60 | 61 | ["SIGINT", "SIGTERM", "SIGQUIT"].forEach((signal) => 62 | process.on(signal, () => process.exit()) 63 | ); 64 | -------------------------------------------------------------------------------- /platform/node/src/instance.ts: -------------------------------------------------------------------------------- 1 | import { numberTo4Bytes } from "../../../lib/bridge/serialization"; 2 | import { callLib } from "./call"; 3 | 4 | type InstanceOpts = { id: string; isEditor: boolean }; 5 | 6 | export function createInstance( 7 | id: InstanceOpts["id"], 8 | isEditor: InstanceOpts["isEditor"] = false 9 | ) { 10 | const header = createPayloadHeader({ id, isEditor }); 11 | 12 | const call = (payload: Uint8Array) => 13 | callLib(new Uint8Array([...header, ...payload])); 14 | 15 | return { 16 | id, 17 | isEditor, 18 | call 19 | }; 20 | } 21 | 22 | const te = new TextEncoder(); 23 | 24 | function createPayloadHeader(opts: InstanceOpts) { 25 | if (opts.isEditor) { 26 | return new Uint8Array([ 27 | 1, // is editor 28 | ...numberTo4Bytes(0) // no project id 29 | ]); 30 | } 31 | 32 | const idData = te.encode(opts.id); 33 | 34 | return new Uint8Array([ 35 | 0, // is not editor 36 | ...numberTo4Bytes(idData.byteLength), // project id length 37 | ...idData // project id 38 | ]); 39 | } 40 | -------------------------------------------------------------------------------- /platform/wasm/.gitignore: -------------------------------------------------------------------------------- 1 | .wrangler -------------------------------------------------------------------------------- /platform/wasm/build.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import esbuild from "esbuild"; 3 | 4 | if (fs.existsSync("out")) { 5 | fs.rmSync("out", { recursive: true }); 6 | } 7 | fs.mkdirSync("out/bin", { recursive: true }); 8 | 9 | fs.cpSync("../../core/bin/wasm.wasm", "out/bin/wasm.wasm"); 10 | fs.cpSync("../../core/bin/wasm.js", "out/bin/wasm.js"); 11 | 12 | const wasmSize = fs.statSync("out/bin/wasm.wasm").size; 13 | 14 | const editorZipFileName = fs 15 | .readdirSync("../../out/zip") 16 | .find((item) => item.startsWith("editor")); 17 | fs.cpSync(`../../out/zip/${editorZipFileName}`, "out/editor.zip"); 18 | 19 | esbuild.buildSync({ 20 | entryPoints: ["src/index.ts"], 21 | outfile: "out/index.js", 22 | bundle: true, 23 | format: "esm", 24 | define: { 25 | "process.env.wasmSize": wasmSize.toString() 26 | } 27 | }); 28 | 29 | ["src/dev-icon.png", "src/index.html"].forEach((f) => 30 | fs.cpSync(f, "out" + f.slice("src".length)) 31 | ); 32 | -------------------------------------------------------------------------------- /platform/wasm/dev.js: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import fs from "fs"; 3 | import open from "open"; 4 | import mimeTypes from "mime-types"; 5 | 6 | const notFound = { 7 | code: 404, 8 | headers: { 9 | "content-type": "text/html" 10 | }, 11 | body: "Not Found" 12 | }; 13 | 14 | const basedir = "out"; 15 | 16 | const existsAndIsFile = (pathname) => { 17 | let stat; 18 | try { 19 | stat = fs.statSync(pathname); 20 | } catch (e) { 21 | return false; 22 | } 23 | 24 | return stat.isFile(); 25 | }; 26 | 27 | const hanlder = (req, res) => { 28 | let pathname = req.url.split("?").shift(); 29 | 30 | if (pathname.startsWith("/")) pathname = pathname.slice(1); 31 | if (pathname.endsWith("/")) pathname = pathname.slice(0, -1); 32 | 33 | pathname = basedir + "/" + pathname; 34 | 35 | if (!existsAndIsFile(pathname)) { 36 | const maybeIndex = pathname + (pathname ? "/" : "") + "index.html"; 37 | if (existsAndIsFile(maybeIndex)) pathname = maybeIndex; 38 | } 39 | 40 | let stats; 41 | 42 | try { 43 | stats = fs.statSync(pathname); 44 | } catch (e) {} 45 | 46 | if (!stats || stats.isDirectory()) { 47 | res.writeHead(notFound.code, notFound.headers); 48 | res.end(notFound.body); 49 | return; 50 | } 51 | 52 | const readStream = fs.createReadStream(pathname); 53 | res.writeHead(200, { 54 | "content-type": mimeTypes.lookup(pathname), 55 | "content-length": stats.size 56 | }); 57 | readStream.pipe(res); 58 | }; 59 | 60 | http.createServer(hanlder).listen(9000, "0.0.0.0"); 61 | open("http://localhost:9000"); 62 | -------------------------------------------------------------------------------- /platform/wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "node build.js", 4 | "start": "npm run build && node dev.js", 5 | "deploy": "npm run build && npx wrangler pages deploy out" 6 | }, 7 | "type": "module", 8 | "dependencies": { 9 | "@types/winbox": "^0.2.5", 10 | "mime-types": "^2.1.35", 11 | "winbox": "^0.2.82" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /platform/wasm/publish.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import url from "node:url"; 3 | import child_process from "node:child_process"; 4 | 5 | const currentDirectory = path.dirname(url.fileURLToPath(import.meta.url)); 6 | const rootDirectory = path.resolve(currentDirectory, "..", ".."); 7 | 8 | // build editor 9 | 10 | child_process.execSync("npm run build -- --production", { 11 | cwd: rootDirectory, 12 | stdio: "inherit" 13 | }); 14 | 15 | // build core 16 | 17 | child_process.execSync("make wasm", { 18 | cwd: path.resolve(rootDirectory, "core", "build"), 19 | stdio: "inherit" 20 | }); 21 | 22 | // build 23 | 24 | child_process.execSync("npm run build", { 25 | cwd: currentDirectory, 26 | stdio: "inherit" 27 | }); 28 | 29 | // upload to CF Pages 30 | 31 | child_process.execSync("npx wrangler pages deploy out", { 32 | cwd: currentDirectory, 33 | stdio: "inherit" 34 | }); 35 | -------------------------------------------------------------------------------- /platform/wasm/src/dev-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/wasm/src/dev-icon.png -------------------------------------------------------------------------------- /platform/windows/.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | *.user 3 | .vs 4 | editor 5 | *.pfx 6 | Package.StoreAssociation.xml 7 | win-*.dll 8 | publish 9 | AppPackages -------------------------------------------------------------------------------- /platform/windows/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /platform/windows/Assets/BadgeLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/BadgeLogo.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/BadgeLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/BadgeLogo.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/BadgeLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/BadgeLogo.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/BadgeLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/BadgeLogo.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/BadgeLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/BadgeLogo.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/Icon-16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Icon-16.ico -------------------------------------------------------------------------------- /platform/windows/Assets/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/LargeTile.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/LargeTile.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/LargeTile.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/LargeTile.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/LargeTile.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SmallTile.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SmallTile.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SmallTile.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SmallTile.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SmallTile.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-16_altform-lightunplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-16_altform-lightunplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-16_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-16_altform-unplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-24_altform-lightunplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-24_altform-lightunplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-256_altform-lightunplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-256_altform-lightunplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-256_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-256_altform-unplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-32_altform-lightunplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-32_altform-lightunplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-32_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-32_altform-unplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/Square44x44Logo.targetsize-48_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Square44x44Logo.targetsize-48_altform-unplated.png -------------------------------------------------------------------------------- /platform/windows/Assets/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /platform/windows/Assets/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /platform/windows/Assets/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /platform/windows/Assets/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /platform/windows/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /platform/windows/Assets/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackedorg/fullstacked/a41baa78aa2537aeb6b68e25a0b17f7f121f7b59/platform/windows/Assets/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /platform/windows/FullStacked.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.11.35327.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FullStacked", "FullStacked.csproj", "{6A45F56D-1305-4B66-A576-D9A095F06A62}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C8EC509E-4140-4AB5-A671-987A5EA88268}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|ARM64 = Debug|ARM64 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|ARM64 = Release|ARM64 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|ARM64.ActiveCfg = Debug|ARM64 21 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|ARM64.Build.0 = Debug|ARM64 22 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|ARM64.Deploy.0 = Debug|ARM64 23 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|x64.ActiveCfg = Debug|x64 24 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|x64.Build.0 = Debug|x64 25 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|x64.Deploy.0 = Debug|x64 26 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|x86.ActiveCfg = Debug|x86 27 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|x86.Build.0 = Debug|x86 28 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Debug|x86.Deploy.0 = Debug|x86 29 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|ARM64.ActiveCfg = Release|ARM64 30 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|ARM64.Build.0 = Release|ARM64 31 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|ARM64.Deploy.0 = Release|ARM64 32 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|x64.ActiveCfg = Release|x64 33 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|x64.Build.0 = Release|x64 34 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|x64.Deploy.0 = Release|x64 35 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|x86.ActiveCfg = Release|x86 36 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|x86.Build.0 = Release|x86 37 | {6A45F56D-1305-4B66-A576-D9A095F06A62}.Release|x86.Deploy.0 = Release|x86 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {471028D8-6CEF-4308-95BF-A5C76ECBE5BE} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /platform/windows/Instance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace FullStacked 5 | { 6 | internal class Instance 7 | { 8 | public String id; 9 | public Boolean isEditor; 10 | 11 | private byte[] header; 12 | 13 | public Instance(String id, Boolean isEditor = false) { 14 | this.id = id; 15 | this.isEditor = isEditor; 16 | 17 | if (isEditor) 18 | { 19 | this.header = new byte[] { 1 }; // isEditor 20 | this.header = App.combineBuffers([this.header, App.numberToByte(0)]); // no project id 21 | } 22 | else { 23 | this.header = new byte[] { 0 }; 24 | byte[] idData = Encoding.UTF8.GetBytes(id); 25 | this.header = App.combineBuffers([this.header, App.numberToByte(idData.Length)]); 26 | this.header = App.combineBuffers([this.header, idData]); 27 | } 28 | } 29 | 30 | public byte[] callLib(byte[] payload) { 31 | byte[] data = App.combineBuffers([this.header, payload]); 32 | 33 | return App.call(data); 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /platform/windows/Lib.cs: -------------------------------------------------------------------------------- 1 | namespace FullStacked 2 | { 3 | unsafe internal abstract class Lib 4 | { 5 | public abstract void setDirectories(void* root, void* config, void* editor); 6 | public abstract void setCallback(CallbackDelegate cb); 7 | 8 | public delegate void CallbackDelegate(string projectId, string messageType, string message); 9 | 10 | public abstract int callLib(byte* payload, int size, byte** response); 11 | public abstract void freePtrLib(void* ptr); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /platform/windows/LibARM64.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace FullStacked 4 | { 5 | unsafe internal class LibARM64 : Lib 6 | { 7 | 8 | const string dllName = "win-arm64.dll"; 9 | 10 | [DllImport(dllName)] 11 | public static extern void directories(void* root, void* config, void* editor); 12 | [DllImport(dllName)] 13 | public static extern void callback(CallbackDelegate cb); 14 | 15 | [DllImport(dllName)] 16 | public static extern int call(byte* payload, int size, byte** response); 17 | [DllImport(dllName)] 18 | public static extern void freePtr(void* ptr); 19 | 20 | public override unsafe void setDirectories(void* root, void* config, void* editor) 21 | { 22 | directories(root, config, editor); 23 | } 24 | 25 | public override void setCallback(CallbackDelegate cb) 26 | { 27 | callback(cb); 28 | } 29 | 30 | public override unsafe int callLib(byte* payload, int size, byte** response) 31 | { 32 | return call(payload, size, response); 33 | } 34 | 35 | public override unsafe void freePtrLib(void* ptr) 36 | { 37 | freePtr(ptr); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /platform/windows/LibX64.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace FullStacked 4 | { 5 | unsafe internal class LibX64 : Lib 6 | { 7 | const string dllName = "win-x64.dll"; 8 | 9 | [DllImport(dllName)] 10 | public static extern void directories(void* root, void* config, void* editor); 11 | [DllImport(dllName)] 12 | public static extern void callback(CallbackDelegate cb); 13 | 14 | [DllImport(dllName)] 15 | public static extern int call(byte* payload, int size, byte** response); 16 | [DllImport(dllName)] 17 | public static extern void freePtr(void* ptr); 18 | 19 | public override unsafe void setDirectories(void* root, void* config, void* editor) 20 | { 21 | directories(root, config, editor); 22 | } 23 | 24 | public override void setCallback(CallbackDelegate cb) 25 | { 26 | callback(cb); 27 | } 28 | 29 | public override unsafe int callLib(byte* payload, int size, byte** response) 30 | { 31 | return call(payload, size, response); 32 | } 33 | 34 | public override unsafe void freePtrLib(void* ptr) 35 | { 36 | freePtr(ptr); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /platform/windows/LibX86.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace FullStacked 5 | { 6 | unsafe internal class LibX86 : Lib 7 | { 8 | const string dllName = "win-x86.dll"; 9 | 10 | [DllImport(dllName)] 11 | public static extern void directories(void* root, void* config, void* editor); 12 | [DllImport(dllName)] 13 | public static extern void callback(CallbackDelegate cb); 14 | 15 | [DllImport(dllName)] 16 | public static extern int call(byte* payload, int size, byte** response); 17 | [DllImport(dllName)] 18 | public static extern void freePtr(void* ptr); 19 | 20 | public override unsafe void setDirectories(void* root, void* config, void* editor) 21 | { 22 | directories(root, config, editor); 23 | } 24 | 25 | public override void setCallback(CallbackDelegate cb) 26 | { 27 | callback(cb); 28 | } 29 | 30 | public override unsafe int callLib(byte* payload, int size, byte** response) 31 | { 32 | return call(payload, size, response); 33 | } 34 | 35 | public override unsafe void freePtrLib(void* ptr) 36 | { 37 | freePtr(ptr); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /platform/windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | FullStacked (Beta) 9 | Charles-Philippe Lepage 10 | Assets\StoreLogo.png 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /platform/windows/Properties/PublishProfiles/win-arm64.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | ARM64 9 | publish\ARM64 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0-windows10.0.19041.0 13 | win-arm64 14 | true 15 | false 16 | false 17 | false 18 | 19 | -------------------------------------------------------------------------------- /platform/windows/Properties/PublishProfiles/win-x64.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | x64 9 | publish\X64 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0-windows10.0.19041.0 13 | win-x64 14 | true 15 | false 16 | false 17 | false 18 | 19 | -------------------------------------------------------------------------------- /platform/windows/Properties/PublishProfiles/win-x86.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | x86 9 | publish\X86 10 | FileSystem 11 | <_TargetId>Folder 12 | net8.0-windows10.0.19041.0 13 | win-x86 14 | true 15 | false 16 | false 17 | false 18 | 19 | -------------------------------------------------------------------------------- /platform/windows/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "windows (Package)": { 4 | "commandName": "MsixPackage" 5 | }, 6 | "windows (Unpackaged)": { 7 | "commandName": "Project" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /platform/windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PerMonitorV2 18 | 19 | 20 | -------------------------------------------------------------------------------- /platform/windows/publish.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import url from "node:url"; 3 | import fs from "node:fs"; 4 | import child_process from "node:child_process"; 5 | import version from "../../version.js"; 6 | 7 | const currentDirectory = path.dirname(url.fileURLToPath(import.meta.url)); 8 | const rootDirectory = path.resolve(currentDirectory, "..", ".."); 9 | 10 | // build editor 11 | 12 | child_process.execSync("npm run build -- --production", { 13 | cwd: rootDirectory, 14 | stdio: "inherit" 15 | }); 16 | 17 | // build core 18 | 19 | child_process.execSync("cmd.exe /c windows.bat", { 20 | cwd: path.resolve(rootDirectory, "core", "build"), 21 | stdio: "inherit" 22 | }); 23 | 24 | // update version 25 | 26 | const winVersion = `${version.major}.${version.minor}.${version.build}.0` 27 | const packageFile = path.resolve(currentDirectory, "Package.appxmanifest"); 28 | let packageContent = fs.readFileSync(packageFile, { encoding: "utf-8" }); 29 | packageContent = packageContent.replace(/\bVersion="\d+\.\d+\.\d+\.\d+"/g, `Version="${winVersion}"`) 30 | fs.writeFileSync(packageFile, packageContent); 31 | 32 | // clean 33 | 34 | const appPackages = path.resolve(currentDirectory, "AppPackages"); 35 | if(fs.existsSync(appPackages)) 36 | fs.rmSync(appPackages, { recursive: true }) 37 | 38 | // msstore 39 | 40 | child_process.execSync("msstore init", { 41 | cwd: currentDirectory, 42 | stdio: "inherit" 43 | }); 44 | child_process.execSync("msstore package", { 45 | cwd: currentDirectory, 46 | stdio: "inherit" 47 | }); 48 | child_process.execSync("msstore publish", { 49 | cwd: currentDirectory, 50 | stdio: "inherit" 51 | }); 52 | -------------------------------------------------------------------------------- /test/core.ts: -------------------------------------------------------------------------------- 1 | import child_process from "child_process"; 2 | import path from "path"; 3 | 4 | child_process.execSync("make clean", { 5 | stdio: "inherit", 6 | cwd: path.resolve(process.cwd(), "core", "build") 7 | }); 8 | 9 | child_process.execSync("make all -j8", { 10 | stdio: "inherit", 11 | cwd: path.resolve(process.cwd(), "core", "build") 12 | }); 13 | 14 | process.exit(0); 15 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import child_process from "child_process"; 2 | import esbuild from "esbuild"; 3 | 4 | const build = (testFile: string) => { 5 | const outfile = "test/.cache/test.js"; 6 | esbuild.buildSync({ 7 | entryPoints: [`test/${testFile}`], 8 | outfile, 9 | bundle: true, 10 | packages: "external", 11 | format: "esm" 12 | }); 13 | return outfile; 14 | }; 15 | 16 | // type checking 17 | child_process.execSync(`node ${build("types.ts")}`, { stdio: "inherit" }); 18 | 19 | // core build 20 | // child_process.execSync(`node ${build("core.ts")}`, { stdio: "inherit" }); 21 | 22 | // basic tests 23 | child_process.execSync(`node ${build("basic.ts")}`, { 24 | stdio: "inherit" 25 | }); 26 | 27 | // deep links and git tests 28 | child_process.execSync(`node ${build("deeplink-git.ts")}`, { 29 | stdio: "inherit" 30 | }); 31 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import child_process from "child_process"; 2 | 3 | // typecheck 4 | child_process.execSync("npm run typecheck", { 5 | stdio: "inherit" 6 | }); 7 | 8 | process.exit(0); 9 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "puppeteer"; 2 | 3 | export const sleep = (ms: number) => 4 | new Promise((resolve) => setTimeout(resolve, ms)); 5 | 6 | export const throwError = (message: string) => { 7 | const error = Error(message); 8 | console.error(error); 9 | process.exit(1); 10 | }; 11 | 12 | export const waitForStackNavigation = (page: Page, selector: string) => { 13 | return new Promise(async (resolve, reject) => { 14 | let clicked = false; 15 | while (!clicked) { 16 | try { 17 | const element = await page.waitForSelector(selector); 18 | await element.click(); 19 | clicked = true; 20 | } catch (e) { 21 | await sleep(100); 22 | } 23 | 24 | if (clicked) { 25 | break; 26 | } 27 | } 28 | await sleep(500); 29 | resolve(); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "ES2022", 5 | "target": "ES2022", 6 | "moduleResolution": "Node" 7 | }, 8 | "exclude": [ 9 | "demo", 10 | "out", 11 | "platform/android", 12 | "platform/apple", 13 | "platform/docker", 14 | "platform/linux", 15 | "platform/node/editor" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import url from "node:url"; 3 | import fs from "node:fs"; 4 | import child_process from "node:child_process"; 5 | 6 | const currentDirectory = path.dirname(url.fileURLToPath(import.meta.url)); 7 | 8 | const packageJsonFile = path.join(currentDirectory, "package.json"); 9 | const packageJsonContent = fs.readFileSync(packageJsonFile, { 10 | encoding: "utf-8" 11 | }); 12 | const packageJson = JSON.parse(packageJsonContent); 13 | 14 | const [major, minor, patch] = packageJson.version.split("."); 15 | 16 | const branch = child_process 17 | .execSync("git rev-parse --abbrev-ref HEAD") 18 | .toString() 19 | .trim(); 20 | const build = child_process 21 | .execSync(`git rev-list --count ${branch}`) 22 | .toString() 23 | .trim(); 24 | 25 | const version = { 26 | major, 27 | minor, 28 | patch, 29 | branch, 30 | build 31 | }; 32 | 33 | export default version; 34 | --------------------------------------------------------------------------------