diff --git a/.gitignore b/.gitignore index 24832ce..0bd7af0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ yarn-error.log* # dev debug debug/photos/**/*.html debug/photos/**/VRChat*wrld* +debug/photos/**/VRChat_*x*.png debug/photos/**/*.txt # local env files diff --git a/README.md b/README.md index 8d6083b..eef0ec3 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,31 @@ -# 作業計画 -## α -* [x] log file の取得 - * log file dir を指定できる - * log file dir から files を読み取って world id と timestamp を取り出す -* [x] ボタンクリックで指定したフォルダ内に固定のファイルを生成できるように -* [x] VRChat の写真 dir を取得できるように -* [x] 指定した dir にファイル生成 -* [x] 月ごとに仕分けしてファイル生成できるように -* [x] package アップグレード -* [x] unused export を怒る lint -* UI と導線整備 - * [ ] わかりやすいオンボーディング -* E2E テスト - -## β -* [x] VRChatLog のライフサイクルを調べる - * ログはいつ消えるの? -* 定期実行できるようにする - -## やらなくてもいいかも -* 今日はどこで何枚写真を撮った、を出す? - - # 動作確認済み開発環境 * GitHub Codespaces -# Installation +## Development +* install all dependencies +```bash +$ yarn +``` -Clone this repo and install all dependencies -`yarn` or `npm install` +* generate files for local debug +```bash +$ yarn setup:debug +``` -## Development +start development server +```bash +$ yarn dev +``` -`yarn dev` or `npm run dev` +* Codespaces を利用している場合 + * `port 6080` をブラウザで開き、password `vscode` を入力することで仮想ウィンドウが立ち上がる -* `port 6080` をブラウザで開き、password `vscode` を入力することで仮想ウィンドウが立ち上がる +### Note * electron background 側の hotreload が効かないのでつらみ ## Build -`yarn build` or `npm run build` +`yarn build` ## Publish diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-01_03-01-18.551_2560x1440.png b/debug/VRChat_2023-10-01_03-01-18.551_2560x1440_sample.png similarity index 100% rename from debug/photos/VRChat/2023-10/VRChat_2023-10-01_03-01-18.551_2560x1440.png rename to debug/VRChat_2023-10-01_03-01-18.551_2560x1440_sample.png diff --git a/debug/cleanDevFiles.sh b/debug/cleanDevFiles.sh new file mode 100644 index 0000000..f368a68 --- /dev/null +++ b/debug/cleanDevFiles.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# スクリプトがエラーに遭遇した場合に終了し、未定義の変数を参照した場合にも終了 +set -eu + +# 引数の解析 +skip_confirmation=false +while getopts "y" opt; do + case $opt in + y) + skip_confirmation=true + ;; + \?) + echo "無効なオプション: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +# スクリプトのディレクトリを取得 +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" + +delete_file_path="$script_dir/photos" +delete_file_names=( + # このツールで作成するJoin記録用の画像ファイル + "VRChat*wrld*.png" + # VRChatで写真を撮ったときに作成される画像ファイル + # (開発環境では `genDevFiles.sh` で作成する`) + "VRChat_*x*.png" +) + +# まず対象の確認 +for delete_file_name in "${delete_file_names[@]}"; do + find $delete_file_path -name $delete_file_name +done + +# 確認 +# -y がついていた場合は確認をスキップ +if [ "$skip_confirmation" = false ]; then + read -p "削除しますか? [y/N]: " yn + case "$yn" in + [yY]*) ;; + *) echo "終了します" ; exit 1 ;; + esac +fi + +# 削除 +for delete_file_name in "${delete_file_names[@]}"; do + find $delete_file_path -name $delete_file_name -delete +done + +echo "削除しました" diff --git a/debug/genDevFiles.sh b/debug/genDevFiles.sh new file mode 100644 index 0000000..122bed8 --- /dev/null +++ b/debug/genDevFiles.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# スクリプトがエラーに遭遇した場合に終了し、未定義の変数を参照した場合にも終了 +set -eu + +# スクリプトのディレクトリを取得 +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" + +# コピー元のファイル(スクリプトの場所からの相対パス) +source_file="$script_dir/VRChat_2023-10-01_03-01-18.551_2560x1440_sample.png" + +# コピー先のファイル名の配列(スクリプトの場所からの相対パス) +destination_files=( + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-44.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-19-50.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-01-18.551_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-47.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-45.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-22-50.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-41.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_02-01-18.551_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_00-01-18.551_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-01_03-01-18.551_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-46.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-40.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-21-50.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-43.163_2560x1440.png" + "./debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-42.163_2560x1440.png" + "./debug/photos/VRChat/2023-11/VRChat_2023-11-08_15-11-42.163_2560x1440.png" + "./debug/photos/VRChat/2023-11/VRChat_2023-11-08_15-12-11.163_2560x1440.png" +) + +# 各ファイルにコピー +for dest_file in "${destination_files[@]}" +do + # ファイルが既に存在する場合はスキップ + if [ ! -f "$dest_file" ]; then + cp "$source_file" "$dest_file" + echo "File $dest_file created." + else + echo "File $dest_file already exists, skipping." + fi +done + + diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_00-01-18.551_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_00-01-18.551_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_00-01-18.551_2560x1440.png and /dev/null differ diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-01-18.551_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-01-18.551_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-01-18.551_2560x1440.png and /dev/null differ diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-19-50.163_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-19-50.163_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-19-50.163_2560x1440.png and /dev/null differ diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-21-50.163_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-21-50.163_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-21-50.163_2560x1440.png and /dev/null differ diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-22-50.163_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-22-50.163_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_01-22-50.163_2560x1440.png and /dev/null differ diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_02-01-18.551_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_02-01-18.551_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_02-01-18.551_2560x1440.png and /dev/null differ diff --git a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-40.163_2560x1440.png b/debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-40.163_2560x1440.png deleted file mode 100644 index a080e9c..0000000 Binary files a/debug/photos/VRChat/2023-10/VRChat_2023-10-08_08-21-40.163_2560x1440.png and /dev/null differ diff --git a/electron/api.ts b/electron/api.ts index b969cc6..64df727 100644 --- a/electron/api.ts +++ b/electron/api.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'events'; import { initTRPC } from '@trpc/server'; import { observable } from '@trpc/server/observable'; import { stackWithCauses } from 'pony-cause'; +import superjson from 'superjson'; import z from 'zod'; // 呼び出し元は集約したい @@ -15,6 +16,7 @@ const ee = new EventEmitter(); const t = initTRPC.create({ isServer: true, + transformer: superjson, }); const logError = (err: Error | string) => { @@ -323,6 +325,43 @@ export const router = t.router({ }, ); }), + getVRChatJoinInfoWithVRChatPhotoList: procedure + .input(z.object({ year: z.string(), month: z.string() })) + .query(async (ctx) => { + const result = await service.getVRChatJoinInfoWithVRChatPhotoList({ + getVRChatPhotoWithWorldIdAndDate: + service.getVRChatPhotoWithWorldIdAndDate, + })({ + year: ctx.input.year, + month: ctx.input.month, + }); + const response: { + data: null | ExtractDataTypeFromResult; + error: null | { + code: string; + message: string; + }; + } = { + data: null, + error: null, + }; + return result.match( + (r) => { + response.data = r; + return response; + }, + (error) => { + logError(error); + return { + data: null, + error: { + code: error.name, + message: `写真の読み込みに失敗しました: ${error.message}`, + }, + }; + }, + ); + }), getVRChatPhotoItemData: procedure.input(z.string()).query(async (ctx) => { const result = await service.getVRChatPhotoItemData(ctx.input); return result.match( diff --git a/electron/service.spec.ts b/electron/service.spec.ts index af0a9e3..64c2e04 100644 --- a/electron/service.spec.ts +++ b/electron/service.spec.ts @@ -1,3 +1,4 @@ +import * as neverthrow from 'neverthrow'; import { getService } from './service'; import { getSettingStore } from './settingStore'; @@ -22,3 +23,201 @@ describe('settingStore', () => { }); }); }); + +describe('getVRChatJoinInfoWithVRChatPhotoList', () => { + type DateTime = { + date: { + year: string; + month: string; + day: string; + }; + time: { + hour: string; + minute: string; + second: string; + millisecond: string; + }; + }; + type PhotoAndJoin = neverthrow.Result< + ( + | { + type: 'PHOTO'; + datetime: DateTime; + path: string; + worldId: null; + } + | { + type: 'JOIN'; + datetime: DateTime; + path: string; + worldId: string; + } + )[], + Error + >; + const createDatetimeByDate = (date: Date) => { + return { + date: { + year: date.getFullYear().toString(), + month: (date.getMonth() + 1).toString().padStart(2, '0'), + day: date.getDate().toString().padStart(2, '0'), + }, + time: { + hour: date.getHours().toString().padStart(2, '0'), + minute: date.getMinutes().toString().padStart(2, '0'), + second: date.getSeconds().toString().padStart(2, '0'), + millisecond: date.getMilliseconds().toString().padStart(3, '0'), + }, + }; + }; + it('createDatetimeByDate', () => { + const result = createDatetimeByDate(new Date('2023-01-01T14:15:00.000Z')); + expect(result).toEqual({ + date: { + year: '2023', + month: '01', + day: '01', + }, + time: { + hour: '14', + minute: '15', + second: '00', + millisecond: '000', + }, + }); + }); + // テスト用の設定を使う + const { getVRChatJoinInfoWithVRChatPhotoList } = getService( + getSettingStore('test-settings'), + ); + it('ちゃんとネストされる', () => { + // テスト用DI関数を用意 + const getVRChatPhotoWithWorldIdAndDate = ({ + year, + month, + }: { year: string; month: string }): PhotoAndJoin => { + return neverthrow.ok([ + { + type: 'PHOTO', + path: `${year}-${month}/photo`, + worldId: null, + datetime: createDatetimeByDate(new Date('2023-12-13T15:00:00.000Z')), + }, + { + type: 'JOIN', + path: `${year}-${month}/join`, + worldId: 'wrld_1234567890', + datetime: createDatetimeByDate(new Date('2023-12-13T14:00:00.000Z')), + }, + ]); + }; + // 関数の戻りが正しいかテスト + const result = getVRChatJoinInfoWithVRChatPhotoList({ + getVRChatPhotoWithWorldIdAndDate, + })({ year: '2023', month: '12' }); + if (result.isErr()) { + throw result.error; + } + expect(result.value).toEqual([ + { + joinDatetime: new Date('2023-12-13T14:00:00.000Z'), + worldId: 'wrld_1234567890', + imgPath: '2023-12/join', + photoList: [ + { + datetime: new Date('2023-12-13T15:00:00.000Z'), + path: '2023-12/photo', + }, + ], + }, + ]); + }); + it('JOINごとにまとまっている PHOTOは直前のJOINにまとまる', () => { + // テスト用DI関数を用意 + const getVRChatPhotoWithWorldIdAndDate = ({ + year, + month, + }: { year: string; month: string }): PhotoAndJoin => { + return neverthrow.ok([ + { + type: 'JOIN', + path: `${year}-${month}/join1`, + worldId: 'wrld_1234567890', + datetime: createDatetimeByDate(new Date('2023-12-13T12:00:00.000Z')), + }, + { + type: 'PHOTO', + path: `${year}-${month}/photo2`, + worldId: null, + datetime: createDatetimeByDate(new Date('2023-12-13T16:00:00.000Z')), + }, + { + type: 'JOIN', + path: `${year}-${month}/join2`, + worldId: 'wrld_1234567890', + datetime: createDatetimeByDate(new Date('2023-12-13T14:00:00.000Z')), + }, + { + type: 'PHOTO', + path: `${year}-${month}/photo1`, + worldId: null, + datetime: createDatetimeByDate(new Date('2023-12-13T15:00:00.000Z')), + }, + { + type: 'JOIN', + path: `${year}-${month}/join3`, + worldId: 'wrld_1234567890', + datetime: createDatetimeByDate(new Date('2023-12-13T18:00:00.000Z')), + }, + { + type: 'PHOTO', + path: `${year}-${month}/photo3`, + worldId: null, + datetime: createDatetimeByDate(new Date('2023-12-13T19:00:00.000Z')), + }, + ]); + }; + // 関数の戻りが正しいかテスト + const result = getVRChatJoinInfoWithVRChatPhotoList({ + getVRChatPhotoWithWorldIdAndDate, + })({ year: '2023', month: '12' }); + if (result.isErr()) { + throw result.error; + } + + expect(result.value).toEqual([ + { + joinDatetime: new Date('2023-12-13T12:00:00.000Z'), + worldId: 'wrld_1234567890', + imgPath: '2023-12/join1', + photoList: [], + }, + { + joinDatetime: new Date('2023-12-13T14:00:00.000Z'), + worldId: 'wrld_1234567890', + imgPath: '2023-12/join2', + photoList: [ + { + datetime: new Date('2023-12-13T15:00:00.000Z'), + path: '2023-12/photo1', + }, + { + datetime: new Date('2023-12-13T16:00:00.000Z'), + path: '2023-12/photo2', + }, + ], + }, + { + joinDatetime: new Date('2023-12-13T18:00:00.000Z'), + worldId: 'wrld_1234567890', + imgPath: '2023-12/join3', + photoList: [ + { + datetime: new Date('2023-12-13T19:00:00.000Z'), + path: '2023-12/photo3', + }, + ], + }, + ]); + }); +}); diff --git a/electron/service.ts b/electron/service.ts index 35bd7ee..9bfdb5a 100644 --- a/electron/service.ts +++ b/electron/service.ts @@ -346,6 +346,33 @@ type DateTime = { millisecond: string; }; }; +const parseDateTime = (datetime: DateTime): Date => { + return datefns.parseISO( + `${datetime.date.year}-${datetime.date.month.padStart( + 2, + '0', + )}-${datetime.date.day.padStart(2, '0')}T${datetime.time.hour.padStart( + 2, + '0', + )}:${datetime.time.minute.padStart(2, '0')}:${datetime.time.second.padStart( + 2, + '0', + )}.${datetime.time.millisecond}Z`, + ); +}; +type PhotoOrJoinImgInfo = + | { + type: 'PHOTO'; + datetime: DateTime; + path: string; + worldId: null; + } + | { + type: 'JOIN'; + datetime: DateTime; + path: string; + worldId: string; + }; const getVRChatPhotoWithWorldIdAndDate = (settingStore: ReturnType) => ({ @@ -354,23 +381,7 @@ const getVRChatPhotoWithWorldIdAndDate = }: { year: string; month: string; - }): neverthrow.Result< - ( - | { - type: 'PHOTO'; - datetime: DateTime; - path: string; - worldId: null; - } - | { - type: 'JOIN'; - datetime: DateTime; - path: string; - worldId: string; - } - )[], - Error - > => { + }): neverthrow.Result => { const result = vrchatPhotoService.getVRChatPhotoItemPathListByYearMonth({ year, month, @@ -425,6 +436,77 @@ const getVRChatPhotoWithWorldIdAndDate = return neverthrow.ok(filteredObjList); }; +interface JoinInfo { + joinDatetime: Date; + worldId: string; + imgPath: string; + photoList: { + datetime: Date; + path: string; + }[]; +} +type GetVRChatJoinInfoWithVRChatPhotoListResult = JoinInfo[]; +const getVRChatJoinInfoWithVRChatPhotoList = + (props: { + getVRChatPhotoWithWorldIdAndDate: ReturnType< + typeof getVRChatPhotoWithWorldIdAndDate + >; + }) => + ({ + year, + month, + }: { year: string; month: string }): neverthrow.Result< + GetVRChatJoinInfoWithVRChatPhotoListResult, + Error + > => { + const result = props.getVRChatPhotoWithWorldIdAndDate({ year, month }); + + if (result.isErr()) { + return neverthrow.err(result.error); + } + + const data = result.value; + + // datetimeに基づいて時系列順にソート + data.sort((a, b) => { + const aDatetime = parseDateTime(a.datetime); + const bDatetime = parseDateTime(b.datetime); + return datefns.compareAsc(aDatetime, bDatetime); + }); + + const joinData: JoinInfo[] = []; + + for (let i = 0; i < data.length; i += 1) { + if (data[i].type === 'JOIN') { + const joinDatetime = parseDateTime(data[i].datetime); + const nextJoinIndex = data.findIndex( + (item, index) => index > i && item.type === 'JOIN', + ); + + const photoList = data + .slice(i + 1, nextJoinIndex !== -1 ? nextJoinIndex : undefined) + .filter((item) => item.type === 'PHOTO') + .map((photo) => ({ + datetime: parseDateTime(photo.datetime), + path: photo.path, + })); + + const { worldId } = data[i]; + if (worldId === null) { + throw new Error('要ロジック修正 data[i].worldId === null'); + } + joinData.push({ + joinDatetime, + worldId, + imgPath: data[i].path, + photoList, + }); + } + } + + return neverthrow.ok(joinData); + }; + const getVRChatPhotoItemDataListByYearMonth = (settingStore: ReturnType) => ( @@ -502,6 +584,7 @@ const getService = (settingStore: ReturnType) => { getVRChatPhotoWithWorldIdAndDate(settingStore), getVRChatPhotoItemData, getVrcWorldInfoByWorldId, + getVRChatJoinInfoWithVRChatPhotoList, }; }; diff --git a/electron/service/viewer.spec.ts b/electron/service/viewer.spec.ts index 1dd96a0..f8fefff 100644 --- a/electron/service/viewer.spec.ts +++ b/electron/service/viewer.spec.ts @@ -79,10 +79,10 @@ describe('viewer_api', () => { .parseJoinInfoFileName(infoFileNameWithoutExt) ._unsafeUnwrap().worldId; // api で world 情報を取得する - const reqUrl = `https://api.vrchat.cloud/api/1/worlds/wrld_${worldId}`; - console.log(reqUrl); + const reqUrl = `https://api.vrchat.cloud/api/1/worlds/${worldId}`; const res = await fetch(reqUrl); const worldInfo = await res.json(); - console.log(worldInfo); + expect(worldInfo.id).toBe(worldId); + expect(typeof worldInfo.name).toBe('string'); }); }); diff --git a/jest.config.js b/jest.config.js index f7526f0..d924477 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,7 @@ module.exports = { '^.+\\.(t|j)sx?$': '@swc/jest', }, testPathIgnorePatterns: ['/node_modules/', '/dist/', '/main/'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, }; diff --git a/package.json b/package.json index 7b50fd3..25eca3d 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,17 @@ "scripts": { "preinstall": "npx only-allow yarn", "postinstall": "simple-git-hooks", - "dev": "eval `dbus-launch --sh-syntax` && concurrently \"yarn dev:vite\" \" yarn dev:electron\"", + "setup:debug": "bash debug/genDevFiles.sh", + "dev": "eval `dbus-launch --sh-syntax` && concurrently \"nr dev:vite\" \" nr dev:electron\"", "dev:vite": "vite", - "dev:electron": "npm run build:electron && electron . --disable-gpu", - "build": "npm run build:vite && npm run build:electron", + "dev:electron": "nr build:electron && electron . --disable-gpu", + "build": "nr build:vite && nr build:electron", "build:vite": "vite build", "build:electron": "tsc -p electron", - "dist": "npm run build && electron-builder --publish never", - "pack": "npm run build && electron-builder --dir", - "clean": "rimraf dist main src/out", + "dist": "nr build && electron-builder --publish never", + "pack": "nr build && electron-builder --dir", + "clean:build": "rimraf dist main src/out", + "clean:debug": "bash debug/cleanDevFiles.sh", "type-check": "tsc --noEmit", "find-deadcode": "ts-prune", "test": "jest", @@ -35,7 +37,7 @@ "lint:fix": "npx @biomejs/biome check --apply ." }, "simple-git-hooks": { - "pre-commit": "yarn lint && yarn type-check" + "pre-commit": "nr lint && nr type-check" }, "engines": { "node": "20", @@ -75,11 +77,12 @@ "react-hot-toast": "^2.4.1", "react-router-dom": "^6.18.0", "sharp": "^0.32.6", + "superjson": "1.13.3", "tailwind-merge": "^2.0.0", "tailwindcss": "^3.3.5", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.0.5", - "vite": "^4.5.1", + "vite": "^4.5.2", "zod": "^3.22.4" }, "devDependencies": { diff --git a/src/components/ui/VrcPhoto.tsx b/src/components/ui/VrcPhoto.tsx index 70db1f5..8fbe247 100644 --- a/src/components/ui/VrcPhoto.tsx +++ b/src/components/ui/VrcPhoto.tsx @@ -15,7 +15,8 @@ export interface PhotoProps extends React.HTMLAttributes { onClickPhoto: (photoPath: string) => void; } -function VrcPhoto({ photoPath, ...props }: PhotoProps) { +const VrcPhoto = ({ photoPath, ...props }: PhotoProps) => { + const photoName = photoPath.split('/').pop(); const query = trpcReact.getVRChatPhotoItemData.useQuery(photoPath); const { data, isLoading } = query; @@ -43,11 +44,11 @@ function VrcPhoto({ photoPath, ...props }: PhotoProps) { -

Open

+

{photoName}

); -} +}; export default VrcPhoto; diff --git a/src/page/photoList/PhotoList.tsx b/src/page/photoList/PhotoList.tsx index d433010..07b66f5 100644 --- a/src/page/photoList/PhotoList.tsx +++ b/src/page/photoList/PhotoList.tsx @@ -15,18 +15,33 @@ const WorldInfo = ({ datetime, }: { vrcWorldId: string; - datetime: { year: string; month: string; day: string }; + datetime: + | { + year: string; + month: string; + day: string; + hour: string; + minute: string; + } + | Date; }) => { const { data } = trpcReact.getVrcWorldInfoByWorldId.useQuery(vrcWorldId, { staleTime: 1000 * 60 * 5, // キャッシュの有効期限を5分に設定 cacheTime: 1000 * 60 * 30, // キャッシュされたデータを30分間メモリに保持 }); + const date = + datetime instanceof Date + ? datetime + : new Date( + `${datetime.year}-${datetime.month}-${datetime.day}T${datetime.hour}:${datetime.minute}:00`, + ); + const dateToDisplay = `${date.getFullYear()}/${ + date.getMonth() + 1 + }/${date.getDate()} ${date.getHours()}:${date.getMinutes()}`; return (

{data?.name ?? vrcWorldId}

-

- Join: {datetime.year}/{datetime.month}/{datetime.day} -

+

Join: {dateToDisplay}

); }; @@ -57,7 +72,7 @@ const PhotoList = () => { const handleOpenFolder = () => { return ( photoItemList?.[0] && - openDirOnExplorerMutatation.mutate(photoItemList[0].path) + openDirOnExplorerMutatation.mutate(photoItemList[0].imgPath) ); }; @@ -130,34 +145,28 @@ const PhotoList = () => {
{photoItemList?.map((item) => { - const content = - item.type === 'PHOTO' ? ( + // item.photoList がある場合は写真一覧を表示する + const photoList = item.photoList.map((photo) => ( +
{ - openPhotoPathMutation.mutate(item.path); + openPhotoPathMutation.mutate(photo.path); }} /> - ) : ( - - ); - - return ( -
- {content}
+ )); + return ( + <> +
+ +
+ {photoList} + ); })}
diff --git a/src/page/photoList/composable.spec.ts b/src/page/photoList/composable.spec.ts new file mode 100644 index 0000000..1b89c9c --- /dev/null +++ b/src/page/photoList/composable.spec.ts @@ -0,0 +1,5 @@ +describe('composable', () => { + it('should be defined', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/src/page/photoList/composable.ts b/src/page/photoList/composable.ts index e6551b1..6e12d17 100644 --- a/src/page/photoList/composable.ts +++ b/src/page/photoList/composable.ts @@ -9,7 +9,6 @@ type YearMonth = { }; export const useYearMonthList = () => { - console.log('useYearMonthList'); const { data: yearMonthList, refetch: refetchYearMonthList } = trpcReact.getVRChatPhotoFolderYearMonthList.useQuery(); const sortedYearMonthList = yearMonthList?.sort((a, b) => { @@ -31,22 +30,22 @@ export const useYearMonthList = () => { }; export const usePhotoItems = (selectedFolderYearMonth: YearMonth) => { - console.log('selectedFolderYearMonth', selectedFolderYearMonth); const [photoItemList, setPhotoItemList] = useState< inferProcedureOutput< - AppRouter['getVRChatPhotoWithWorldIdAndDate'] + AppRouter['getVRChatJoinInfoWithVRChatPhotoList'] >['data'] >(); + const [photoItemFetchError, setPhotoItemFetchError] = useState< inferProcedureOutput< - AppRouter['getVRChatPhotoWithWorldIdAndDate'] + AppRouter['getVRChatJoinInfoWithVRChatPhotoList'] >['error'] >(null); const photoItemListQuery = - trpcReact.getVRChatPhotoWithWorldIdAndDate.useQuery( + trpcReact.getVRChatJoinInfoWithVRChatPhotoList.useQuery( selectedFolderYearMonth, { enabled: !!( @@ -55,36 +54,10 @@ export const usePhotoItems = (selectedFolderYearMonth: YearMonth) => { }, ); - const sortedPhotoItems = useMemo(() => { - if (!photoItemListQuery.data?.data) { - return []; - } - - return [...photoItemListQuery.data.data].sort((a, b) => { - const datetimeA = - a.datetime.date.year + - a.datetime.date.month + - a.datetime.date.day + - a.datetime.time.hour + - a.datetime.time.minute + - a.datetime.time.second + - a.datetime.time.millisecond; - const datetimeB = - b.datetime.date.year + - b.datetime.date.month + - b.datetime.date.day + - b.datetime.time.hour + - b.datetime.time.minute + - b.datetime.time.second + - b.datetime.time.millisecond; - return datetimeB.localeCompare(datetimeA); - }); - }, [photoItemListQuery.data?.data]); - useEffect(() => { - setPhotoItemList(sortedPhotoItems); + setPhotoItemList(photoItemListQuery.data?.data); setPhotoItemFetchError(photoItemListQuery.data?.error ?? null); - }, [sortedPhotoItems, photoItemListQuery.data?.error]); + }, [photoItemListQuery.data?.data, photoItemListQuery.data?.error]); const refetchPhotoItemList = () => photoItemListQuery.refetch(); diff --git a/src/trpcWrapper.tsx b/src/trpcWrapper.tsx index 34417eb..956412a 100644 --- a/src/trpcWrapper.tsx +++ b/src/trpcWrapper.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ipcLink } from 'electron-trpc/renderer'; import React, { useState } from 'react'; import { toast } from 'react-hot-toast'; +import superjson from 'superjson'; import { trpcReact } from './trpc'; @@ -43,6 +44,7 @@ export default ({ children }: { children: React.ReactNode }) => { const [trpcClient] = useState(() => trpcReact.createClient({ links: [ipcLink()], + transformer: superjson, }), ); return ( diff --git a/tsconfig.json b/tsconfig.json index bdff415..2b6f52a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ }, "sourceMap": true, "target": "ESNext", + "module": "ESNext", "lib": ["DOM", "DOM.Iterable", "ESNext"], "types": ["vite/client", "node", "electron", "jest"], "allowJs": false, @@ -14,7 +15,6 @@ "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, - "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, diff --git a/yarn.lock b/yarn.lock index 916c570..fb75ea2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2861,6 +2861,13 @@ convert-source-map@^2.0.0: resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +copy-anything@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" + integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== + dependencies: + is-what "^4.1.8" + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4662,6 +4669,11 @@ is-weakset@^2.0.1: call-bind "^1.0.2" get-intrinsic "^1.1.1" +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" @@ -6799,6 +6811,13 @@ sumchecker@^3.0.1: dependencies: debug "^4.1.0" +superjson@1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.13.3.tgz#3bd64046f6c0a47062850bb3180ef352a471f930" + integrity sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg== + dependencies: + copy-anything "^3.0.2" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" @@ -7289,10 +7308,10 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vite@^4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a" - integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA== +vite@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82" + integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w== dependencies: esbuild "^0.18.10" postcss "^8.4.27"