diff --git a/index.html b/index.html index 85d4346..10d15eb 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + + + + + + diff --git a/scripts/release.ts b/scripts/release.ts index beb7b2c..68183f7 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -7,21 +7,21 @@ execSync(`tauri build --target x86_64-apple-darwin`, { stdio: "inherit" }) execSync(`rm -rf release && mkdir release`) execSync( - `cp src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Dropcode.app.tar.gz release/Dropcode_${tauriConf.package.version}_aarch64.app.tar.gz` + `cp src-tauri/target/aarch64-apple-darwin/release/bundle/macos/GameNotebook.app.tar.gz release/GameNotebook_${tauriConf.package.version}_aarch64.app.tar.gz` ) execSync( - `cp src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Dropcode.app.tar.gz.sig release/Dropcode_${tauriConf.package.version}_aarch64.app.tar.gz.sig` + `cp src-tauri/target/aarch64-apple-darwin/release/bundle/macos/GameNotebook.app.tar.gz.sig release/GameNotebook_${tauriConf.package.version}_aarch64.app.tar.gz.sig` ) execSync( - `cp src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/Dropcode_${tauriConf.package.version}_aarch64.dmg release/` + `cp src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/GameNotebook_${tauriConf.package.version}_aarch64.dmg release/` ) execSync( - `cp src-tauri/target/x86_64-apple-darwin/release/bundle/macos/Dropcode.app.tar.gz release/Dropcode_${tauriConf.package.version}_x64.app.tar.gz` + `cp src-tauri/target/x86_64-apple-darwin/release/bundle/macos/GameNotebook.app.tar.gz release/GameNotebook_${tauriConf.package.version}_x64.app.tar.gz` ) execSync( - `cp src-tauri/target/x86_64-apple-darwin/release/bundle/macos/Dropcode.app.tar.gz.sig release/Dropcode_${tauriConf.package.version}_x64.app.tar.gz.sig` + `cp src-tauri/target/x86_64-apple-darwin/release/bundle/macos/GameNotebook.app.tar.gz.sig release/GameNotebook_${tauriConf.package.version}_x64.app.tar.gz.sig` ) execSync( - `cp src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/Dropcode_${tauriConf.package.version}_x64.dmg release/` + `cp src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/GameNotebook_${tauriConf.package.version}_x64.dmg release/` ) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a219424..ac0ff01 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -541,16 +541,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "dropcode" -version = "0.0.0" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-build", -] - [[package]] name = "dtoa" version = "0.4.8" @@ -740,6 +730,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gamenotebook" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", +] + [[package]] name = "gdk" version = "0.15.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c2f3e59..af3fcf9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "dropcode" +name = "gamenotebook" version = "0.0.0" description = "A Tauri App" -authors = ["you"] +authors = ["Jesse Freeman"] license = "" repository = "" edition = "2021" diff --git a/src-tauri/icons/macos/AppIcon.icns b/src-tauri/icons/macos/AppIcon.icns index f8b88d5..6422086 100644 Binary files a/src-tauri/icons/macos/AppIcon.icns and b/src-tauri/icons/macos/AppIcon.icns differ diff --git a/src-tauri/icons/macos/AppIconAlt2.icns b/src-tauri/icons/macos/AppIconAlt2.icns new file mode 100755 index 0000000..1a4d96f Binary files /dev/null and b/src-tauri/icons/macos/AppIconAlt2.icns differ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8f6c422..dd0763e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "distDir": "../dist" }, "package": { - "productName": "Dropcode", + "productName": "GameNotepad", "version": "0.0.7" }, "tauri": { @@ -30,13 +30,13 @@ "icons/web/icon-192.ico", "icons/macos/AppIcon.icns" ], - "identifier": "dev.egoist.dropcode", + "identifier": "com.pixelvision8.gamenotepad", "longDescription": "", "macOS": { "entitlements": null, "exceptionDomain": "", "frameworks": [], - "providerShortName": "GJE9R5VE87", + "providerShortName": "PV8GNPD", "signingIdentity": null }, "resources": [], @@ -56,7 +56,7 @@ "dialog": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIxQ0E3OEIyOEZBM0U0OUIKUldTYjVLT1BzbmpLSWNxSlVoRTFJbjdkZS9OdHFXS2VtbUlXd0tDL0psbG1xM1pIU2pjZGtoV0IK", "endpoints": [ - "https://updater.egoist.dev/check/egoist/dropcode/{{target}}/{{arch}}/{{current_version}}" + "https://updater.egoist.dev/check/egoist/gamenotebook/{{target}}/{{arch}}/{{current_version}}" ] }, "windows": [ @@ -64,7 +64,7 @@ "fullscreen": false, "height": 600, "resizable": true, - "title": "Dropcode", + "title": "GameNotepad", "width": 800, "minWidth": 800, "minHeight": 600, diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index a3d7ed7..1e16ac5 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -1,53 +1,201 @@ -import { basicSetup, EditorView } from "codemirror" -import { EditorState, type Extension } from "@codemirror/state" -import { createEffect, createSignal, onCleanup, onMount } from "solid-js" -import { githubDark, githubLight } from "@uiw/codemirror-theme-github" -import { useDarkMode } from "../lib/darkmode" +import { basicSetup, EditorView } from "codemirror"; +import { EditorState, EditorSelection, Transaction, type Extension } from "@codemirror/state"; +import { keymap } from "@codemirror/view"; +import { createEffect, createSignal, onCleanup, onMount } from "solid-js"; +import { githubDark, githubLight } from "@uiw/codemirror-theme-github"; +import { useDarkMode } from "../lib/darkmode"; +import { autocompletion, startCompletion } from '@codemirror/autocomplete'; +// const { StateCommand, Selection } = EditorState; +type StateCommand = (context: {state: EditorState, dispatch: (tr: Transaction) => void}) => boolean; + +interface CompletionResult { + from: number; + to: number; + options: {label: string, type: string}[]; +} + +interface CompletionContext { + state: EditorState; + pos: number; + explicit: boolean; + matchBefore(expr: RegExp): {from: number, to: number, text: string} | null; +} + +type CompletionSource = (context: CompletionContext) => CompletionResult | null | Promise; + +// The Editor component. +// It accepts the following props: +// - `value`: the initial content of the editor +// - `onChange`: a callback function to handle when the content of the editor changes +// - `extensions`: additional extensions to enhance the functionality of the editor export const Editor = (props: { value: string onChange: (newValue: string) => void extensions?: Extension[] }) => { + // Create a variable to hold the HTML element where the EditorView will be rendered. let el: HTMLDivElement | undefined + + // Create a reactive signal for EditorView. const [getView, setView] = createSignal() + + // Check if the application is running in dark mode. const isDarkMode = useDarkMode() + // This is a list of PV8's APIs for demonstration purposes + // You should replace this with the actual list of APIs + const pixelVisionAPI = [ + { label: "BackgroundColor", signature: "BackgroundColor(id)", type: "function", info: "Manages system colors and the background color used to clear the display." }, + { label: "Color", signature: "Color(id, value)", type: "function", info: "Allows you to read and update color values in the ColorChip." }, + { label: "DrawRect", signature: "DrawRect(x, y, width, height, color, drawMode)", type: "function", info: "Displays a rectangle with a fill color on the screen." }, + { label: "DrawMetaSprite", signature: "DrawMetaSprite(id, x, y, flipH, flipV, drawMode, colorOffset)", type: "function", info: "Draws a Sprite Collection to the display." }, + { label: "DrawSprite", signature: "DrawSprite(id, x, y, flipH, flipV, drawMode, colorOffset)", type: "function", info: "Draws a single sprite to the display." }, + { label: "DrawText", signature: "DrawText(text, x, y, drawMode, font, colorOffset, spacing)", type: "function", info: "Renders text to the display." }, + { label: "ReadSaveData", signature: "ReadSaveData(key, defaultValue)", type: "function", info: "Reads saved data by supplying a key." }, + { label: "WriteSaveData", signature: "WriteSaveData(key, value)", type: "function", info: "Writes saved data by supplying a key and value." }, + { label: "Button", signature: "Button(Buttons button, InputState state, int controllerID)", type: "function", info: "Gets the current state of any button by calling the Button() method and supplying a button ID." }, + { label: "Key", signature: "Key(key, state)", type: "function", info: "Tests for keyboard input by calling the Key() API." }, + { label: "MouseButton", signature: "MouseButton(int button, InputState state)", type: "function", info: "Gets the current state of the mouse's left (0) and right (1) buttons by calling MouseButton()API." }, + { label: "MousePosition", signature: "MousePosition()", type: "function", info: "Returns a Point for the mouse cursor's X and Y position." }, + { label: "Init", signature: "Init()", type: "function", info: "Called when a game first loads up." }, + { label: "Draw", signature: "Draw()", type: "function", info: "Called once per frame after the Update() has been completed." }, + { label: "Update", signature: "Update(timeDelta)", type: "function", info: "Called once per frame at the beginning of the game loop." }, + { label: "PauseSong", signature: "PauseSong()", type: "function", info: "Toggles the current playback state of the sequencer." }, + { label: "PlaySong", signature: "PlaySong(id, loop, startAt)", type: "function", info: "Activates the MusicChip's tracker to playback any of the songs stored in memory." }, + { label: "RewindSong", signature: "RewindSong(position, patternID)", type: "function", info: "Rewinds the currently playing song to a specific position and pattern ID." }, + { label: "StopSong", signature: "StopSong()", type: "function", info: "Stops the currently playing song." }, + { label: "Display", signature: "Display()", type: "function", info: "Gets the resolution of the display at run time." }, + { label: "RedrawDisplay", signature: "RedrawDisplay()", type: "function", info: "Executes both the Clear() and DrawTilemap() APIs in a single call." }, + { label: "ScrollPosition", signature: "ScrollPosition(x, y)", type: "function", info: "Scrolls the tilemap by calling the ScrollPosition() API and supplying a new scroll X and Y position." }, + { label: "PlaySound", signature: "PlaySound(id, channel)", type: "function", info: "Plays a single sound effect on a specific channel." }, + { label: "Sound", signature: "Sound(int id, string data)", type: "function", info: "Reads raw sound data from the SoundChip." }, + { label: "StopSound", signature: "StopSound(channel)", type: "function", info: "Stops any sound playing on a specific channel." }, + { label: "Sprite", signature: "Sprite(id, data)", type: "function", info: "Reads and writes pixel data directly to the SpriteChip's memory." }, + { label: "TotalSprites", signature: "TotalSprites(bool ignoreEmpty)", type: "function", info: "Returns the total number of sprites in the SpriteChip." }, + { label: "Flag", signature: "Flag(column, row, value)", type: "function", info: "Quickly accesses just the flag value of a tile." }, + { label: "Tile", signature: "Tile(column, row, spriteID, colorOffset, flag, flipH, flipV)", type: "function", info: "Gets the current sprite, color offset and flag values associated with a given tile ID." }, + { label: "TilemapSize", signature: "TilemapSize(width, height, clear)", type: "function", info: "Returns a Pointrepresenting the size of the tilemap in columns(X) and rows (Y)." }, + { label: "UpdateTiles", signature: "UpdateTiles(ids, column, row, width, colorOffset, flag)", type: "function", info: "Updates the color offset and flag values of multiple tiles at once." }, + ]; + + // A state signal to hold the currently selected option's info. + const [getSelectedOptionInfo, setSelectedOptionInfo] = createSignal(''); + + const completionSource: CompletionSource = (context: CompletionContext) => { + const beforeCursor = context.state.sliceDoc(0, context.pos); + const match = /\b\w+$/.exec(beforeCursor); + + if (!match) { + return null; + } + + const wordStart = match.index; + + return { + from: wordStart, + to: context.pos, + options: pixelVisionAPI.map(api => ({ + label: api.signature, + type: api.type, + })), + }; + }; + + // Then you can use this completion source when you configure autocompletion + const autocompleteExtension = autocompletion({ override: [completionSource] }); + + const insertTab: StateCommand = ({state, dispatch}) => { + // Get the current selection. + let {from, to} = state.selection.main; + // If there's a selection, delete it. + if (from !== to) { + const tr = state.update({changes: {from, to, insert: ""}}); + dispatch(tr); + } + // Insert four spaces at the current cursor position. + const tr = state.update({changes: {from, to: from, insert: " "}}); + dispatch(tr); + // Move the caret to the end of the inserted spaces. + const trSelection = state.update({selection: EditorSelection.single(from + 4)}); + dispatch(trSelection); + return true; +}; + +const deleteSpaces: StateCommand = ({state, dispatch}) => { + // Get the current selection. + let {from, to} = state.selection.main; + // If there's a selection, delete it. + if (from !== to) { + const tr = state.update({changes: {from, to, insert: ""}}); + dispatch(tr); + } + // If the four characters before the cursor are spaces, delete them. + else if (state.doc.sliceString(from - 4, from) === " ") { + const tr = state.update({changes: {from: from - 4, to: from, insert: ""}}); + dispatch(tr); + } + return true; +}; + + + const tabKeymap = keymap.of([ + { key: "Tab", run: insertTab }, + { key: "Shift-Tab", run: deleteSpaces }, + { key: "Alt-Space", run: startCompletion} + ]); + + + // Run the following code after the component is mounted. onMount(() => { + // Create an update listener that gets triggered when there are any changes in the EditorView. const handleUpdate = EditorView.updateListener.of((update) => { - const value = update.state.doc.toString() - props.onChange(value) + const value = update.state.doc.toString() // Convert the current document in the EditorView to a string. + props.onChange(value) // Call the onChange function with the new value. }) + // Define a function to create a new EditorView. const createView = () => { return new EditorView({ - parent: el, + parent: el, // Parent element is the HTML element defined above. state: EditorState.create({ - doc: "", + doc: "", // Initial content of the editor. extensions: [ + // Add different extensions based on whether the app is in dark mode or not. isDarkMode() ? githubDark : githubLight, basicSetup, + autocompletion({ + override: [completionSource], // Use the PV8 completion source + }), + // startCompletion, // Start completion immediately handleUpdate, EditorView.lineWrapping, ...(props.extensions || []), + tabKeymap, + ], }), }) } + // Create an effect that creates a new EditorView and cleans up when it's no longer needed. createEffect(() => { - const view = createView() - setView(view) + const view = createView() // Create a new EditorView. + setView(view) // Set the view signal to the newly created EditorView. + // Cleanup function to be run when the EditorView is unmounted. onCleanup(() => { - view.destroy() + view.destroy() // Destroy the EditorView. }) }) + // Create an effect that updates the content of the EditorView whenever the value prop changes. createEffect(() => { - const view = getView() - if (!view) return - const oldValue = view.state.doc.toString() + const view = getView() // Get the current EditorView. + if (!view) return // If there is no EditorView, do nothing. + const oldValue = view.state.doc.toString() // Get the current content of the EditorView. + // If the value prop is different from the current content of the EditorView, + // dispatch an action to update the content. if (props.value !== oldValue) { view.dispatch({ changes: { from: 0, to: oldValue.length, insert: props.value }, @@ -56,5 +204,6 @@ export const Editor = (props: { }) }) + // Render a div element where the EditorView will be created. return
} diff --git a/src/components/Game.tsx b/src/components/Game.tsx new file mode 100644 index 0000000..42d273d --- /dev/null +++ b/src/components/Game.tsx @@ -0,0 +1,137 @@ +import { createSignal, onCleanup, onMount } from "solid-js"; + +interface CustomWindow extends Window { + GetURL?: () => string; + LogToJavaScript2?: (message: string) => void; + createUnityInstance?: (canvas: HTMLCanvasElement, config: object) => Promise; +} + +interface UnityInstance { + Module: { + canvas: HTMLCanvasElement; + // other properties... + }; + Quit: () => void; + // other methods... +} + + +let unityInstance: UnityInstance | undefined; // Declare a variable to hold the Unity game instance + +declare let window: CustomWindow; + +export const Game = () => { + let canvas: HTMLCanvasElement; + let gameContainer: HTMLDivElement; + + const [width, setWidth] = createSignal(0); + const [height, setHeight] = createSignal(0); + + const scaleToFit = true; // replace with your actual value + const r = 256 / 240; + + const onResize = () => { + + if (!canvas || !gameContainer) { + return; // Exit the function if canvas or gameContainer is null + } + + let w = 0; + let h = 0; + + if (scaleToFit) { + w = gameContainer.offsetWidth; + h = gameContainer.offsetHeight; + + const r = 512 / 480; // Adjusted aspect ratio + + if (w / r > h) { + w = Math.floor(h * r); + } else { + h = Math.floor(w / r); + } + } + + if (canvas && gameContainer) { + canvas.style.width = `${w}px`; + canvas.style.height = `${h}px`; + canvas.style.position = 'absolute'; + canvas.style.top = `${Math.floor((gameContainer.offsetHeight - h) / 2)}px`; + canvas.style.left = `${Math.floor((gameContainer.offsetWidth - w) / 2)}px`; + } + + setWidth(w); + setHeight(h); + }; + + onCleanup(() => { + + if (unityInstance) { + unityInstance.Quit(); // Stop the Unity game instance + } + + window.removeEventListener('resize', onResize); + delete window.GetURL; + delete window.LogToJavaScript2; + + // Remove event listeners from the canvas + // if (canvas) { + // canvas.removeEventListener('mousemove', mouseMoveHandler); + // canvas.removeEventListener('mousedown', mouseDownHandler); + // canvas.removeEventListener('mouseup', mouseUpHandler); + // // Add more lines here if you have other event listeners + // } + }); + + + window.addEventListener('resize', onResize); + + onMount(() => { + + // Define the GetURL function + + window.GetURL = () => { + console.log("GetURL called!!!!"); + return "/UnityWebGL/StreamingAssets/game.pv8"; + }; + + // Define the LogToJavaScript function + window.LogToJavaScript2 = (message) => { + console.log("From Unity: " + message); + }; + + // Unity game configuration + const config = { + canvasId: "unity-canvas", + dataUrl: "/UnityWebGL/Build/UnityWebGL.data.gz", + frameworkUrl: "/UnityWebGL/Build/UnityWebGL.framework.js.gz", + codeUrl: "/UnityWebGL/Build/UnityWebGL.wasm.gz", + streamingAssetsUrl: "/UnityWebGL/StreamingAssets", + companyName: "Pixel Vision 8", + productName: "Pixel Vision 8 Unity Runner", + productVersion: "1.0", + }; + + // Check if createUnityInstance is defined before calling it + if (window.createUnityInstance) { + window.createUnityInstance(canvas, config).then(function (instance) { + canvas = instance.Module.canvas; + onResize(); + }); + } else { + console.error("window.createUnityInstance is not defined"); + } + }); + + return ( +
{ gameContainer = el; }} style={{ position: 'relative', width: '100%', height: '100%', background: 'black' }}> + { canvas = el; }} + data-pixel-art="true" + style={{ position: 'absolute' }} + /> +
+ ) + +} diff --git a/src/lib/languages.ts b/src/lib/languages.ts index 0345c2e..e058306 100644 --- a/src/lib/languages.ts +++ b/src/lib/languages.ts @@ -39,206 +39,206 @@ export const languages: { id: "plaintext", name: "Plain Text", }, - { - id: "javascript", - name: "JavaScript", - extension: () => javascript(), - }, - { - id: "jsx", - name: "JSX", - extension: () => javascript({ jsx: true }), - }, - { - id: "typescript", - name: "TypeScript", - extension: () => javascript({ typescript: true }), - }, - { - id: "tsx", - name: "TSX", - extension: () => javascript({ typescript: true, jsx: true }), - }, - { - id: "python", - name: "Python", - extension: () => python(), - }, - { - id: "rust", - name: "Rust", - extension: () => rust(), - }, - { - id: "css", - name: "CSS", - extension: () => css(), - }, - { - id: "html", - name: "HTML", - extension: () => html(), - }, - { - id: "vue", - name: "Vue", - extension: () => html(), - }, - { - id: "svelte", - name: "Svelte", - extension: () => html(), - }, - { - id: "xml", - name: "XML", - extension: () => xml(), - }, + // { + // id: "javascript", + // name: "JavaScript", + // extension: () => javascript(), + // }, + // { + // id: "jsx", + // name: "JSX", + // extension: () => javascript({ jsx: true }), + // }, + // { + // id: "typescript", + // name: "TypeScript", + // extension: () => javascript({ typescript: true }), + // }, + // { + // id: "tsx", + // name: "TSX", + // extension: () => javascript({ typescript: true, jsx: true }), + // }, + // { + // id: "python", + // name: "Python", + // extension: () => python(), + // }, + // { + // id: "rust", + // name: "Rust", + // extension: () => rust(), + // }, + // { + // id: "css", + // name: "CSS", + // extension: () => css(), + // }, + // { + // id: "html", + // name: "HTML", + // extension: () => html(), + // }, + // { + // id: "vue", + // name: "Vue", + // extension: () => html(), + // }, + // { + // id: "svelte", + // name: "Svelte", + // extension: () => html(), + // }, + // { + // id: "xml", + // name: "XML", + // extension: () => xml(), + // }, { id: "markdown", name: "Markdown", extension: () => markdown(), }, - { - id: "cpp", - name: "C++", - extension: () => cpp(), - }, - { - id: "c", - name: "C", - extension: () => cpp(), - }, + // { + // id: "cpp", + // name: "C++", + // extension: () => cpp(), + // }, + // { + // id: "c", + // name: "C", + // extension: () => cpp(), + // }, { id: "csharp", name: "C#", extension: () => cpp(), }, - { - id: "java", - name: "Java", - extension: () => java(), - }, - { - id: "sql", - name: "SQL", - extension: () => sql(), - }, - { - id: "php", - name: "PHP", - extension: () => php(), - }, - { - id: "ruby", - name: "Ruby", - extension: () => StreamLanguage.define(ruby), - }, - { - id: "go", - name: "Go", - extension: () => StreamLanguage.define(go), - }, - { - id: "erlang", - name: "Erlang", - extension: () => StreamLanguage.define(erlang), - }, - { - id: "haskell", - name: "Haskell", - extension: () => StreamLanguage.define(haskell), - }, + // { + // id: "java", + // name: "Java", + // extension: () => java(), + // }, + // { + // id: "sql", + // name: "SQL", + // extension: () => sql(), + // }, + // { + // id: "php", + // name: "PHP", + // extension: () => php(), + // }, + // { + // id: "ruby", + // name: "Ruby", + // extension: () => StreamLanguage.define(ruby), + // }, + // { + // id: "go", + // name: "Go", + // extension: () => StreamLanguage.define(go), + // }, + // { + // id: "erlang", + // name: "Erlang", + // extension: () => StreamLanguage.define(erlang), + // }, + // { + // id: "haskell", + // name: "Haskell", + // extension: () => StreamLanguage.define(haskell), + // }, { id: "lua", name: "Lua", extension: () => StreamLanguage.define(lua), }, - { - id: "nginx", - name: "Ngix", - extension: () => StreamLanguage.define(nginx), - }, - { - id: "swift", - name: "Swift", - extension: () => StreamLanguage.define(swift), - }, - { - id: "yaml", - name: "YAML", - extension: () => StreamLanguage.define(yaml), - }, - { - id: "toml", - name: "TOML", - extension: () => StreamLanguage.define(toml), - }, - { - id: "clojure", - name: "Clojure", - extension: () => StreamLanguage.define(clojure), - }, - { - id: "crystal", - name: "Crystal", - extension: () => StreamLanguage.define(crystal), - }, - { - id: "dockerfile", - name: "Dockerfile", - extension: () => StreamLanguage.define(dockerFile), - }, - { - id: "sass", - name: "Sass", - extension: () => StreamLanguage.define(sass), - }, + // { + // id: "nginx", + // name: "Ngix", + // extension: () => StreamLanguage.define(nginx), + // }, + // { + // id: "swift", + // name: "Swift", + // extension: () => StreamLanguage.define(swift), + // }, + // { + // id: "yaml", + // name: "YAML", + // extension: () => StreamLanguage.define(yaml), + // }, + // { + // id: "toml", + // name: "TOML", + // extension: () => StreamLanguage.define(toml), + // }, + // { + // id: "clojure", + // name: "Clojure", + // extension: () => StreamLanguage.define(clojure), + // }, + // { + // id: "crystal", + // name: "Crystal", + // extension: () => StreamLanguage.define(crystal), + // }, + // { + // id: "dockerfile", + // name: "Dockerfile", + // extension: () => StreamLanguage.define(dockerFile), + // }, + // { + // id: "sass", + // name: "Sass", + // extension: () => StreamLanguage.define(sass), + // }, { id: "json", name: "JSON", extension: () => json(), }, - { - id: "powershell", - name: "PowerShell", - extension: () => StreamLanguage.define(powerShell), - }, - { - id: "dart", - name: "Dart", - extension: () => StreamLanguage.define(dart), - }, - { - id: "kotlin", - name: "Kotlin", - extension: () => StreamLanguage.define(kotlin), - }, - { - id: "scala", - name: "Scala", - extension: () => StreamLanguage.define(scala), - }, - { - id: "r", - name: "R", - extension: () => StreamLanguage.define(r), - }, - { - id: "ocaml", - name: "OCaml", - extension: () => StreamLanguage.define(oCaml), - }, - { - id: "fsharp", - name: "F#", - extension: () => StreamLanguage.define(fSharp), - }, - { - id: "commonlisp", - name: "Common Lisp", - extension: () => StreamLanguage.define(commonLisp), - }, + // { + // id: "powershell", + // name: "PowerShell", + // extension: () => StreamLanguage.define(powerShell), + // }, + // { + // id: "dart", + // name: "Dart", + // extension: () => StreamLanguage.define(dart), + // }, + // { + // id: "kotlin", + // name: "Kotlin", + // extension: () => StreamLanguage.define(kotlin), + // }, + // { + // id: "scala", + // name: "Scala", + // extension: () => StreamLanguage.define(scala), + // }, + // { + // id: "r", + // name: "R", + // extension: () => StreamLanguage.define(r), + // }, + // { + // id: "ocaml", + // name: "OCaml", + // extension: () => StreamLanguage.define(oCaml), + // }, + // { + // id: "fsharp", + // name: "F#", + // extension: () => StreamLanguage.define(fSharp), + // }, + // { + // id: "commonlisp", + // name: "Common Lisp", + // extension: () => StreamLanguage.define(commonLisp), + // }, ].sort((a, b) => { return a.name < b.name ? -1 : 1 }) diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index d167b75..1abef18 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -1,15 +1,34 @@ +// Import useNavigate from Solid's router. This is a hook that gives us a function +// that we can use to programmatically navigate to other routes in our app. import { useNavigate } from "@solidjs/router" + +// Import onMount from Solid. This is a lifecycle hook that we can use to run +// some code when this component is first mounted to the DOM. import { onMount } from "solid-js" + +// Import useOpenFolderDialog from a local file. This seems to be a custom hook +// that presumably opens a file dialog when called, and returns the selected folder. import { useOpenFolderDialog } from "../lib/open-folder" + +// Import state from another local file. This is likely the global state of your app, +// probably managed with Solid's built-in reactive state system. import { state } from "../store" +// Define a functional component called Home. export const Home = () => { + // Get the navigate function from the useNavigate hook. const goto = useNavigate() + // Get the openFolder function from the useOpenFolderDialog hook. const openFolder = useOpenFolderDialog() + // When the component is first mounted to the DOM, this code will run. onMount(() => { + // Get the first folder from the app state. const firstFolder = state.app.folders[0] + + // If there's a first folder, navigate to the /snippets route, + // and pass the folder as a query parameter. if (firstFolder) { goto( `/snippets?${new URLSearchParams({ folder: firstFolder }).toString()}` @@ -17,13 +36,21 @@ export const Home = () => { } }) + // The component returns some JSX, which is the UI of the component. + // This will be a button in the center of the screen that, when clicked, + // opens the open folder dialog. return (
+ + {/* Container for the new snippet, search, and trash buttons */}
+ {/* Button to create a new snippet */} + + {/* Button to toggle the search box */} + + {/* Button to toggle the visibility of snippets in the trash */}
+ + {/* Only show the following elements when getSearchType() returns a truthy value */} + {/* A div that contains the search or trash control elements */}
+ {/* Flex container for aligning elements horizontally */}
+ {/* Display either 'Trash' or 'Search' based on the value of getSearchType() */} {getSearchType() === "trash" ? "Trash" : "Search"} + {/* Only show the following 'Empty' button when getSearchType() returns 'trash' */} + {/* 'Empty' button for emptying the trash. It's disabled when there are no snippets */}
+ {/* Input container for searching */}
+ {/* Search input field */} setSearchKeyword(e.currentTarget.value)} + // Event listener for keypress. If 'Escape' is pressed, setSearchType is set to null onKeyPress={(e) => { if (e.key === "Escape") { - e.preventDefault() - setSearchType(null) + e.preventDefault(); + setSearchType(null); } }} /> @@ -351,10 +450,14 @@ export const Snippets = () => {
+ + {/* Sidebar body with a custom scrollbar. The .group/sidebar-body classes might be a part of a custom library or framework */} + { class="h-full w-full flex items-center justify-center px-20 text-center text-zinc-400 text-xl" > - Select or create a snippet from sidebar + {/* The fallback message when no snippet is selected */} + Select or create a game script from sidebar } > -
+ {/* Main content area when a snippet is selected */} +
+
+ {/* Header section of the selected snippet */}
+ {/* Input field for the name of the selected snippet */} + {/* Action buttons for selected snippet */}
+ {/* Button for selecting the language of the snippet - icon came from https://icon-sets.iconify.design/majesticons/play-circle-line/ and use majesticons:stop-circle-line + for the stop button*/} +
+ +
+ + {/* Adding the new "Run" button */} + {/* Button for showing more options */}
+ {/* Dropdown for more options */}
-
- + {/* Main body section for displaying the content of the snippet added a hack to bring the bottom of the editor panel up to remove additional scroll bar */} +
+ {/* Editor component for editing the content of the snippet */} + + + + + +
+ {/* Footer section */}
+ {/* Modals for language selection, folder history, and VSCode snippet settings */} { snippetId={getOpenVSCodeSnippetSettingsModal()} close={() => setOpenVSCodeSnippetSettingsModal(undefined)} /> + {/* Button for moving multiple selected snippets to trash or restoring them */}
{ class="cursor inline-flex items-center bg-white dark:bg-zinc-700 rounded-lg shadow border px-3 h-9 hover:bg-zinc-100" onClick={moveSelectedSnippetsToTrashOrRestore} > + {/* Conditionally display the button label based on whether the snippets are in the trash */} {getSearchType() === "trash" ? `Restore ${actualSelectedSnippetIds().length} snippets from Trash` : `Move ${actualSelectedSnippetIds().length} snippets to Trash`}
- ) -} + ); +}; diff --git a/src/store.ts b/src/store.ts index 330b6c0..fcfbb21 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,245 +1,342 @@ -import { nanoid } from "nanoid" -import { fs, path, dialog, os } from "@tauri-apps/api" -import { BaseDirectory } from "@tauri-apps/api/fs" -import { createStore } from "solid-js/store" +// import statements for required libraries and modules +import { nanoid } from "nanoid"; +import { fs, path, dialog, os } from "@tauri-apps/api"; +import { BaseDirectory } from "@tauri-apps/api/fs"; +import { createStore } from "solid-js/store"; +// Interface for the Snippet data structure export interface Snippet { - id: string - name: string - createdAt: string - updatedAt: string - language?: string - deletedAt?: string + id: string; + name: string; + createdAt: string; + updatedAt: string; + language?: string; + deletedAt?: string; vscodeSnippet?: { - prefix?: string - } + prefix?: string; + }; } +// Interface for AppData containing folders array interface AppData { - folders: string[] + folders: string[]; } +// Creating a store to manage the application state const [state, setState] = createStore<{ - ready: boolean - app: AppData - folder: string | null - snippets: Snippet[] - isMac: boolean + ready: boolean; + app: AppData; + folder: string | null; + snippets: Snippet[]; + isMac: boolean; }>({ - ready: false, + ready: false, // Indicates if the application is ready app: { - folders: [], + folders: [], // Array to store folder paths }, - folder: null, - snippets: [], - isMac: /macintosh/i.test(navigator.userAgent), -}) - -export { state } - + folder: null, // Stores the selected folder path, initially set to null + snippets: [], // Array to store snippets + isMac: /macintosh/i.test(navigator.userAgent), // Boolean flag to detect if the user is on a Mac +}); + +// Exporting the application state +export { state }; +// Function to write the snippets data to a JSON file const writeSnippetsJson = async (folder: string, snippets: Snippet[]) => { - console.log("writing snippets.json") + console.log("writing snippets.json"); // Logging a message for debugging purposes await fs.writeTextFile( await path.join(folder, "snippets.json"), JSON.stringify(snippets) - ) -} + ); +}; +// Function to write the application data to a JSON file const writeAppJson = async (appData: AppData) => { - await fs.createDir("", { dir: BaseDirectory.App, recursive: true }) + await fs.createDir("", { dir: BaseDirectory.App, recursive: true }); await fs.writeTextFile("app.json", JSON.stringify(appData), { dir: BaseDirectory.App, - }) -} + }); +}; +// Function to check if a path exists const pathExists = async (path: string, baseDir?: BaseDirectory) => { - const exists: boolean = await fs.exists(path, { dir: baseDir }) - return exists -} + const exists: boolean = await fs.exists(path, { dir: baseDir }); + return exists; +}; +// Object containing various actions to be performed export const actions = { + // Action to initialize the application init: async () => { const text = await fs .readTextFile("app.json", { dir: BaseDirectory.App }) .catch((error) => { - console.error(error) - return "{}" - }) - const appData: Partial = JSON.parse(text) + console.error(error); + return "{}"; + }); + const appData: Partial = JSON.parse(text); if (appData.folders) { - setState("app", "folders", appData.folders) + setState("app", "folders", appData.folders); } - setState("ready", true) + setState("ready", true); // Mark the application as ready }, + // Action to set the selected folder setFolder: (folder: string | null) => { - setState("folder", folder) + setState("folder", folder); }, + // Action to remove a folder from history removeFolderFromHistory: async (folder: string) => { setState( "app", "folders", state.app.folders.filter((f) => f !== folder) - ) - await writeAppJson(state.app) + ); + await writeAppJson(state.app); }, + // Continuing with the 'actions' object containing various actions + + // Action to load a selected folder and its snippets loadFolder: async (folder: string) => { - const exists = await pathExists(folder) + const exists = await pathExists(folder); + // Check if the selected folder exists if (!exists) { - await actions.removeFolderFromHistory(folder) - await dialog.message("Folder doesn't exist") - return + // If the folder doesn't exist, remove it from the history and show an error message + setState( + "app", + "folders", + state.app.folders.filter((f) => f !== folder) + ); + await writeAppJson(state.app); + await dialog.message("A 'Workspace' folder doesn't exist"); + return; } - const snippetsPath = await path.join(folder, "snippets.json") + + // Get the path to the "snippets.json" file inside the selected folder + const snippetsPath = await path.join(folder, "snippets.json"); + + // Read the contents of "snippets.json" file const text = await fs.readTextFile(snippetsPath).catch((error) => { - console.error(error) - return null - }) + console.error(error); + return null; + }); + if (text) { - const snippets = JSON.parse(text) - setState("snippets", snippets) + // If the file exists and is not empty, parse the JSON data and update the snippets state + const snippets = JSON.parse(text); + setState("snippets", snippets); } else { - setState("snippets", []) + // If the file is empty or doesn't exist, set the snippets state to an empty array + setState("snippets", []); } + // Update the history of selected folders based on the user's choice if (state.app.folders.includes(folder)) { setState("app", "folders", [ folder, ...state.app.folders.filter((f) => f !== folder), - ]) + ]); } else { - setState("app", "folders", [folder, ...state.app.folders.slice(0, 10)]) + setState("app", "folders", [folder, ...state.app.folders.slice(0, 10)]); } - await writeAppJson(state.app) + // Write the updated application data to "app.json" file + await writeAppJson(state.app); }, + // Action to create a new snippet createSnippet: async (snippet: Snippet, content: string) => { - if (!state.folder) return + if (!state.folder) return; + + // Generate a unique ID for the new snippet + const snippetId = actions.getRandomId(); + + // Get the filepath where the snippet content will be saved + const filepath = await path.join(state.folder, snippetId); + + // Write the snippet content to the file + await fs.writeTextFile(filepath, content); - const filepath = await path.join(state.folder, snippet.id) - await fs.writeTextFile(filepath, content) - const snippets = [...state.snippets, snippet] - await writeSnippetsJson(state.folder, snippets) - setState("snippets", snippets) + // Update the snippets state with the new snippet + const snippets = [...state.snippets, { ...snippet, id: snippetId }]; + await writeSnippetsJson(state.folder, snippets); + setState("snippets", snippets); }, + // Action to get a random ID for a snippet getRandomId: () => { - return nanoid(10) + return nanoid(10); }, + // Action to read the content of a snippet given its ID readSnippetContent: async (id: string) => { - if (!state.folder) return "" - const text = await fs.readTextFile(await path.join(state.folder, id)) - return text + if (!state.folder) return ""; + + // Get the filepath of the snippet based on its ID + const filepath = await path.join(state.folder, id); + + // Read the content of the snippet from the file + const text = await fs.readTextFile(filepath); + return text; }, + // Action to update a snippet's property value updateSnippet: async ( id: string, key: K, value: V ) => { - if (!state.folder) return + if (!state.folder) return; + // Update the specified property of the snippet with the new value const snippets = state.snippets.map((snippet) => { if (snippet.id === id) { - return { ...snippet, [key]: value, updatedAt: new Date().toISOString() } + return { + ...snippet, + [key]: value, + updatedAt: new Date().toISOString(), + }; } - return snippet - }) + return snippet; + }); - setState("snippets", snippets) + // Update the snippets state with the updated snippet + setState("snippets", snippets); - await writeSnippetsJson(state.folder, snippets) - await actions.syncSnippetsToVscode() + // Write the updated snippets data to "snippets.json" file + await writeSnippetsJson(state.folder, snippets); + + // Call the 'syncSnippetsToVscode' action to synchronize the snippets with VSCode + await actions.syncSnippetsToVscode(); }, + // Action to update a snippet's content updateSnippetContent: async (id: string, content: string) => { - if (!state.folder) return + if (!state.folder) return; + + // Get the filepath of the snippet based on its ID + const filepath = await path.join(state.folder, id); + + // Write the new content to the snippet file + await fs.writeTextFile(filepath, content); - await fs.writeTextFile(await path.join(state.folder, id), content) - await actions.updateSnippet(id, "updatedAt", new Date().toISOString()) + // Update the 'updatedAt' property of the snippet + await actions.updateSnippet(id, "updatedAt", new Date().toISOString()); }, + // Continuing with the 'actions' object containing various actions + + // Action to move snippets to trash or restore them moveSnippetsToTrash: async (ids: string[], restore = false) => { - if (!state.folder) return + if (!state.folder) return; + // Update the 'deletedAt' property of the specified snippets to move them to trash or restore them const snippets = state.snippets.map((snippet) => { if (ids.includes(snippet.id)) { return { ...snippet, deletedAt: restore ? undefined : new Date().toISOString(), - } + }; } - return snippet - }) + return snippet; + }); + + // Update the snippets state with the updated snippets + setState("snippets", snippets); - setState("snippets", snippets) + // Write the updated snippets data to "snippets.json" file + await writeSnippetsJson(state.folder, snippets); - await writeSnippetsJson(state.folder, snippets) - await actions.syncSnippetsToVscode() + // Call the 'syncSnippetsToVscode' action to synchronize the snippets with VSCode + await actions.syncSnippetsToVscode(); }, + // Action to delete a snippet permanently deleteSnippetForever: async (id: string) => { - if (!state.folder) return + if (!state.folder) return; - const snippets = state.snippets.filter((snippet) => { - return id !== snippet.id - }) - await writeSnippetsJson(state.folder, snippets) - await fs.removeFile(await path.join(state.folder, id)) - setState("snippets", snippets) + // Filter out the snippet with the specified ID from the snippets state + const snippets = state.snippets.filter((snippet) => snippet.id !== id); + + // Update the snippets state with the filtered snippets + setState("snippets", snippets); + + // Write the updated snippets data to "snippets.json" file + await writeSnippetsJson(state.folder, snippets); + + // Delete the snippet file permanently from the folder + await fs.removeFile(await path.join(state.folder, id)); }, + // Action to empty the trash and delete snippets permanently emptyTrash: async () => { - if (!state.folder) return - const toDelete: string[] = [] + if (!state.folder) return; + + const toDelete: string[] = []; + + // Filter out the snippets with 'deletedAt' property set (i.e., in trash) const snippets = state.snippets.filter((snippet) => { if (snippet.deletedAt) { - toDelete.push(snippet.id) + toDelete.push(snippet.id); } - return !snippet.deletedAt - }) - await writeSnippetsJson(state.folder, snippets) + return !snippet.deletedAt; + }); + + // Update the snippets state with the filtered snippets + setState("snippets", snippets); + + // Write the updated snippets data to "snippets.json" file + await writeSnippetsJson(state.folder, snippets); + + // Delete the snippets permanently from the folder await Promise.all( toDelete.map(async (id) => { - return fs.removeFile(await path.join(state.folder!, id)) + return fs.removeFile(await path.join(state.folder!, id)); }) - ) - setState("snippets", snippets) + ); }, + // Continuing with the 'actions' object containing various actions + + // Action to get the folder history from "folders.json" file getFolderHistory: async () => { const text = await fs .readTextFile("folders.json", { dir: BaseDirectory.App }) - .catch(() => "[]") - const folders: string[] = JSON.parse(text) - return folders + .catch(() => "[]"); + const folders: string[] = JSON.parse(text); + return folders; }, + // Action to synchronize snippets with Visual Studio Code (VSCode) syncSnippetsToVscode: async () => { - if (!state.folder) return + if (!state.folder) return; - const folderName = state.folder.split(path.sep).pop()! + // Get the name of the current folder from the selected folder path + const folderName = state.folder.split(path.sep).pop()!; + // Type definition for VSCode snippets type VSCodeSnippets = Record< string, { scope: string; prefix: string[]; body: string[]; __folderName: string } - > + >; - const newSnippets: VSCodeSnippets = {} + // Object to store new snippets data to be synchronized with VSCode + const newSnippets: VSCodeSnippets = {}; + // Iterate through snippets to build new snippets data for (const s of state.snippets) { - const prefix = s.vscodeSnippet?.prefix?.trim() + const prefix = s.vscodeSnippet?.prefix?.trim(); + // Exclude snippets with no prefix or those that are marked as deleted (in trash) if (!prefix || s.deletedAt) { - continue + continue; } + // Construct the new snippet data newSnippets[s.name] = { scope: "", prefix: prefix @@ -248,19 +345,20 @@ export const actions = { .filter(Boolean), body: [await actions.readSnippetContent(s.id)], __folderName: folderName, - } + }; } - const snippetsFileName = "dropcode.code-snippets" - const codeSnippetsDir = `Code${path.sep}User${path.sep}snippets` - const snippetsFilePath = `${codeSnippetsDir}${path.sep}${snippetsFileName}` + // File and directory information for VSCode snippets + const snippetsFileName = "gamenotebook.code-snippets"; + const codeSnippetsDir = `Code${path.sep}User${path.sep}snippets`; + const snippetsFilePath = `${codeSnippetsDir}${path.sep}${snippetsFileName}`; - // VSCode is not installed + // Check if VSCode is installed (based on the presence of "Code" directory in user's data directory) if (!(await pathExists("Code", BaseDirectory.Data))) { - return + return; // VSCode is not installed, so exit the action } - // Get existing snippets + // Get existing snippets from the VSCode snippets file, if it exists const snippets: VSCodeSnippets = (await pathExists( snippetsFilePath, BaseDirectory.Data @@ -268,27 +366,27 @@ export const actions = { ? JSON.parse( await fs.readTextFile(snippetsFilePath, { dir: BaseDirectory.Data }) ) - : {} + : {}; - // Merge old and new snippets + // Merge old and new snippets, remove existing snippets from the same folder for (const name in snippets) { - const snippet = snippets[name] + const snippet = snippets[name]; if (snippet.__folderName === folderName) { - delete snippets[name] + delete snippets[name]; } } - Object.assign(snippets, newSnippets) + Object.assign(snippets, newSnippets); - // Write to file - console.log("writing", snippetsFilePath) + // Write the updated snippets data to the VSCode snippets file + console.log("writing", snippetsFilePath); await fs.createDir(codeSnippetsDir, { recursive: true, dir: BaseDirectory.Data, - }) + }); await fs.writeTextFile( snippetsFilePath, JSON.stringify(snippets, null, 2), { dir: BaseDirectory.Data } - ) + ); }, -} +};