この記事を作った動機

 2025/9/24 から、今日に至るまでの間で OneNote 代替に関して、実装をするなど研究を進めているうちにわかったことなどを簡易的に記録する。

フロンエンド関連

動作の様子

ネットワーク関連

データの扱いを変更

 websocket 経由でデータサーバとフロントエンドがやり取りする型をフロントエンド側に拡張する前提で汎用的に定義した。自分で data key の項目は書かなければならない。(database.tsx)

// frontend -> dataserver
export interface baseResponseTypesFromDataserver{
  status: string;
  errorMessage: string;
  UUID: string | null;
  command: string | null;
}

// frontend -> datasever
export interface baseRequestTypesFromFromtend{
  command: string,
  UUID: string,
}

// dataserver -> frontend
export interface baseInterruptRequestFromDataserver{
  componentName: string,
  command: string,
  UUID: string
}

 以下は、(createNotebook.tsx) における baseResponseTypes の例

import { type baseResponseTypes } from "../network/database";

interface pageType extends baseResponseTypes{
    data: []
}

ネットワーク送信処理の共通化

 database.tsx で共通の websocket でデータをバックエンドサーバに送るための専用のコードを書いた。接続中や切断されていてまだ再接続がされていないときは、 interval 秒後に再試行する仕組みにした。

export function send(websocket:WebSocket, request:string){
  if(
    websocket.readyState != WebSocket.CONNECTING  && 
    websocket.readyState != WebSocket.CLOSED      &&
    websocket.readyState != WebSocket.CLOSING
  ){
    setTimeout(() => { websocket.send(request) },interval * 1000)
  }
}

データサーバの再接続

 database.tsx で再接続する仕組みを書いた。最初は、複数の再接続用の関数が並列で動いてしまったり、他のホットリロード動いていたフロントエンドと新しく読み込み直したフロントエンドが干渉してうまく動作確認できなかったりはしたが、とりあえず動くところまで持ってきた。

 ただ、データサーバとの接続が途切れたとか、再接続中であることをユーザーに示す UI がまだなのは課題であり、messageboxの実装ができ次第実装しようと考えてはいる。

export function useDatabaseEffects() {
  const serverIP = useDatabaseStore((s) => s.serverIP);
  const setWebsocket = useDatabaseStore.setState;
  const getWebsocket = useDatabaseStore((s) => s.getWebsocket)
  // const init = useRef(true)

  useEffect(() => {
    if (!serverIP) return;

    const reconnectLoop = () => { 
      console.log("reconnect observer")
      const websocket = getWebsocket()
      if(websocket != null){
        if(websocket.readyState == WebSocket.OPEN){
          // observe the connection
          setTimeout(() => { 
            reconnectLoop()
          },interval * 1000)
          return
        }
      }
      setTimeout(() => { 
        const websocket = new WebSocket(serverIP);
        // websocket.addEventListener("error",reconnectLoop)
        // websocket.addEventListener("open",reconnectLoop)
        setWebsocket({ websocket: websocket });
      },interval / 2 * 1000)
      setTimeout(() => { 
        reconnectLoop()
      },interval * 1000)
    } 
    const websocket = new WebSocket(serverIP);
    websocket.addEventListener("error",reconnectLoop)
    websocket.addEventListener("open",reconnectLoop)
    setWebsocket({ websocket: websocket });

    return () => {
      websocket.close();
    };
  }, [serverIP, setWebsocket]);
}

markdown view の実装

 markdwon の表示を marked ライブラリで行うようにし、sytle は別途デフォルトのHTMLのスタイルを参考に指定した。

markdを使ったmarkdownViewの実装

messagebox

 messageBox を実装しようと考えた。まだ実装できていないが、各コンポーネントがユーザーにエラーなどを表示できる共通のUIを実装したいと考えている

create notebook 機能の検証

 create notebook を機能するように調整しようとしたが、まだ不完全なようである。

  • uuid.uuid4 が UUID 型を返しており、str へのキャストが必要だった
  • loadSettings.py で、相対パスが指定されていたら、python が実行されたワーキングディレクトリに".“を置き換えるようにしていたはずが、機能していなかったので修正した。

overlayWindow の実装

  • z-index について設定を考えた。基本的には、overlay window に関しては、大多数のOSで採用されているようなウィンドウシステムの挙動を真似ることにした。

  • overlay window について、アクティブ、非アクティブの実装をしようと思った。アクティブなものは不透明に、非アクティブなものは、透過度を強める。

  • overlay window について、アクティブ、非アクティブの実装はできたが、z-indexが正しく更新できないバグが発生している。 ー> ある程度は治った 状態変数の使い方を間違っていた。 https://www.nyanmo.info/posts/webdevelop/react/zustand/

  • ovarlayWindow でウィンドウが隠れないように、画面外に移動できないように制限をした

ToggleToolsBar の実装

  • すべてのウィンドウを閉じるボタンを作った

    ToggleToolsBar の実装の説明用画像 すべてのウィンドウを閉じるボタン

  • タブ機能を実装し、すべてを tools bar に詰め込まないようにする予定。現時点では、タブを選択するためのウィンドを表示する緑のボタンと、タブ選択用のウィンドウをつくった。

    ToggleToolsBar の実装の説明用画像 タブ機能の実装途中の様子

free ページの実装

  • free ページのオブジェクトの要素として、縦と横のサイズプロパティを追加した。
  • とりあえず free ページでテキストを表示できるようにした。そしてとりあえず動かせるようにした。
  • リッチなテキスト、例えば初期気設定ができるなどについては、あとからmarkdownを使って専用の要素として余裕があれば実装することを考えた。
freepageの現状

tailwindcss の挙動

 tailwindcss では、基本的に元々 className 等として静的にスタイルが指定されていないと、正しく動作しないことがわかった。スクリプトでクラス名を制御しても、色が正しく反映されないないなどの症状になることがわかった。

 そこで、あらかじめ背景色については、ある程度事前に使えるようにすることにした。具体的には、見えない要素を作って、その要素のクラス名にひたすら一部を除くすべての背景色を className として指定し、ユーザーからは見えないが、スクリプト上では表示していることになるようにした。

  • ColorPalette.tsx
// force tailwindcss to add color style

export function ColorPalette(){
    return <div>
        <div className="bg-yellow-100"></div>
        <div className="bg-yellow-200"></div>
        <div className="bg-yellow-300"></div>
        ...
        <div className="bg-stone-900"></div>
        <div className="bg-stone-950"></div>
    </div>
}
  • window.tsx
// ...何かしらのコード

// show selector of notebooks, pages and files
export default function Window(){    
    return(
        <>
            <div className="window flex flex-row z-2">
                <ColorPalette></ColorPalette>
                <ToggleToolsBar></ToggleToolsBar>

                <Page></Page>

                <Selector></Selector>
                <CreateNotebook></CreateNotebook>
                <CreatePage></CreatePage>
                <DeleteNotebook></DeleteNotebook>
                <DeletePage></DeletePage>

                <StartButtonMenu></StartButtonMenu>                

            </div>
        </>
    )
}

その他

削除系ボタン

 とりあえず、ノートとページ削除ボタンをフロントエンド側に作った。まだ具体的な実装はない。

状態変数は設定直後に読み込むと内容が反映されない?

export function CreatePage(){
...
    const [pageType,setPageType] = useState<pageType | null>(null)
...

    useEffect(() => {
	...
        const handleMessage = (event:MessageEvent) => {
            const result = JSON.parse(String(event.data));
            console.log(result)

            // dataserver -> frontend


            // frontend -> dataserver
            if(result.UUID == requestUUID.current && "getPageType" == result.command){
                if (!result.status.includes("error")) {
                    setPageType(result)
		    console.log("websocket on message")
		    console.log(pageType) // null
                } else {
                    setPageType({
                        status: result.status,
                        errorMessage: result.errorMessage,
                        UUID: result.UUID,
                        command: result.command,
                        data: null,
                    });
                }
            }
        }

        websocket.addEventListener("message",handleMessage)
        return () => {
            websocket.removeEventListener("message",handleMessage)
        }
    }

    useEffect(() => {
	console.log("useEffect: pageType") 
	console.log(pageType) // non null
    },[pageType])

}

バックエンド関連

getPageType を実装した

 データサーバのバックエンド側で、getPageType コマンドを実装した。具体的には、どんなページの種類が利用可能かフロントエンドに伝えるものである。例えば以下のようなものである。

 このコマンドは、ページを新規作成するときに主にフロントエンドが、どんな種類のページが利用可能か、データサーバに問い合わせるためにある。

リクエスト例

{
    "command"   : "getPageType",
    "UUID"      : "UUID string",
    "data"      : { }
}

レスポンス例

{
    "status": "ok",
    "errorMessage": "nothing",
    "UUID": "e2e920de-42ad-4d79-bdab-7877cbcb8747",
    "command": "getPageType",
    "data": [
        "free",
        "markdown"
    ]
}

UUID をページ識別用に追加

{
    "pageType": "free",
    "tags": ["This","is","testpage"],
    "files": ["testfile.txt"],
    "UUID": "950b0810-702c-4489-bbce-bc9fdd9f0b22", // 追加部分
    "pageData":{
        "items":[
            {
                "ID": "3d3751c3-20ee-4124-90df-464860620f4a",
                "type": "text",
                "data": "This is test text. Yukkuri reimu said 'ゆっくりしていってね'.",
                "position":{
                    "x": 200,
                    "y": 100,
                    "z": 0
                }
            },
            {
                "ID": "ef3429f5-f150-4ad2-a325-80257fad8f6b",
                "type": "text",
                "data": "This is test text. Yukkuri marisa abuse Yukkuri reimu.",
                "position":{
                    "x": 200,
                    "y": 150,
                    "z": 1
                }
            }
        ]
    }
}

マークダウンにメタデータ情報を結局埋め込んだ

++++
{
    files:["なんかよくわかんない絵ジト目口開き3.png"], 
    tags:[],
    UUID:"6428f4c7-7622-40b9-a6bc-f895b3f9ada8"
}
++++

# hello world
This is test content.
...

バックエンドのヘルパー関数の実装

  • データサーババックエンドに、ファイルやフォルダ消去用の関数をhelper.commonのdeleteDataSafely関数として追加した。

バックエンドについて

  • リモートサーバを使うにしても、結局キャッシュのために dataserver をローカルで動かすので、clientside は dataserver は統合してしまってもいいかもしれないと考えた。

参考にしたサイトとか